public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Gabriel Goller <g.goller@proxmox.com>
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	[thread overview]
Message-ID: <20260203160246.353351-6-g.goller@proxmox.com> (raw)
In-Reply-To: <20260203160246.353351-1-g.goller@proxmox.com>

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 <g.goller@proxmox.com>
---
 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 <!nocheck>,
  rustc:native (>= 1.82) <!nocheck>,
  libstd-rust-dev <!nocheck>,
  librust-anyhow-1+default-dev <!nocheck>,
+ librust-bon-3+default-dev (>= 3.7-~~) <!nocheck>,
+ librust-minijinja-2+default-dev (>= 2.5-~~) <!nocheck>,
+ librust-minijinja-2+loader-dev (>= 2.5-~~) <!nocheck>,
+ librust-minijinja-2+multi-template-dev (>= 2.5-~~) <!nocheck>,
  librust-proxmox-network-types-0.1+default-dev (>= 0.1.1-~~) <!nocheck>,
  librust-proxmox-sdn-types-0.1+default-dev <!nocheck>,
+ librust-proxmox-serde-1+default-dev <!nocheck>,
  librust-serde-1+default-dev <!nocheck>,
  librust-serde-1+derive-dev <!nocheck>,
  librust-serde-repr-0.1+default-dev <!nocheck>,
@@ -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<openfabric::OpenfabricRouter> for Router {
-    fn from(value: openfabric::OpenfabricRouter) -> Self {
-        Router::Openfabric(value)
-    }
-}
-
-/// Generic FRR routername.
-///
-/// The variants represent different protocols. Some have `router <protocol> <name>`, others have
-/// `router <protocol> <process-id>`, some only have `router <protocol>`.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum RouterName {
-    Openfabric(openfabric::OpenfabricRouterName),
-    Ospf(ospf::OspfRouterName),
-}
-
-impl From<openfabric::OpenfabricRouterName> for RouterName {
-    fn from(value: openfabric::OpenfabricRouterName) -> Self {
-        Self::Openfabric(value)
-    }
-}
-
-impl Display for RouterName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Self::Openfabric(r) => r.fmt(f),
-            Self::Ospf(r) => r.fmt(f),
-        }
-    }
-}
-
-/// 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 <name>
-/// ! ...
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
-pub enum Interface {
-    Openfabric(openfabric::OpenfabricInterface),
-    Ospf(ospf::OspfInterface),
-}
-
-impl From<openfabric::OpenfabricInterface> for Interface {
-    fn from(value: openfabric::OpenfabricInterface) -> Self {
-        Self::Openfabric(value)
-    }
-}
-
-impl From<ospf::OspfInterface> for Interface {
-    fn from(value: ospf::OspfInterface) -> Self {
-        Self::Ospf(value)
-    }
-}
-
 #[derive(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<str> for FrrWord {
     fn as_ref(&self) -> &str {
         &self.0
@@ -157,7 +64,7 @@ impl AsRef<str> 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, Self::Error> {
         Self::new(value)
     }
 }
 
-impl TryFrom<String> for CommonInterfaceName {
-    type Error = CommonInterfaceNameError;
+impl TryFrom<String> for InterfaceName {
+    type Error = InterfaceNameError;
 
     fn try_from(value: String) -> Result<Self, Self::Error> {
         Self::new(value)
     }
 }
 
-impl CommonInterfaceName {
-    pub fn new<T: AsRef<str> + Into<String>>(s: T) -> Result<Self, CommonInterfaceNameError> {
+impl InterfaceName {
+    pub fn new<T: AsRef<str> + Into<String>>(s: T) -> Result<Self, InterfaceNameError> {
         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<T> {
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub addresses: Vec<Cidr>,
+
+    #[serde(flatten)]
+    pub properties: T,
+}
+impl From<openfabric::OpenfabricInterface> for Interface<openfabric::OpenfabricInterface> {
+    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<RouterName, Router>,
-    pub interfaces: BTreeMap<InterfaceName, Interface>,
-    pub access_lists: Vec<AccessList>,
-    pub routemaps: Vec<RouteMap>,
-    pub protocol_routemaps: BTreeSet<ProtocolRouteMap>,
+impl From<ospf::OspfInterface> for Interface<ospf::OspfInterface> {
+    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<Item = (&RouterName, &Router)> + '_ {
-        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<InterfaceName>,
+}
 
-    pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
-        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<Item = &AccessList> + '_ {
-        self.access_lists.iter()
-    }
-    pub fn routemaps(&self) -> impl Iterator<Item = &RouteMap> + '_ {
-        self.routemaps.iter()
-    }
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct IpProtocolRouteMap {
+    pub v4: Option<RouteMapName>,
+    pub v6: Option<RouteMapName>,
+}
 
-    pub fn protocol_routemaps(&self) -> impl Iterator<Item = &ProtocolRouteMap> + '_ {
-        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<FrrProtocol, IpProtocolRouteMap>,
+
+    #[builder(default)]
+    #[serde(default)]
+    pub routemaps: BTreeMap<RouteMapName, Vec<RouteMapEntry>>,
+    #[builder(default)]
+    #[serde(default)]
+    pub access_lists: BTreeMap<AccessListName, Vec<AccessListRule>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub struct OpenfabricFrrConfig {
+    #[serde(default)]
+    pub router: BTreeMap<openfabric::OpenfabricRouterName, openfabric::OpenfabricRouter>,
+    #[serde(default)]
+    pub interfaces: BTreeMap<InterfaceName, Interface<openfabric::OpenfabricInterface>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub struct OspfFrrConfig {
+    #[serde(default)]
+    pub router: Option<ospf::OspfRouter>,
+    #[serde(default)]
+    pub interfaces: BTreeMap<InterfaceName, Interface<ospf::OspfInterface>>,
 }
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<FrrWord> 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<bool>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
     pub hello_interval: Option<proxmox_sdn_types::openfabric::HelloInterval>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
     pub csnp_interval: Option<proxmox_sdn_types::openfabric::CsnpInterval>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
     pub hello_multiplier: Option<proxmox_sdn_types::openfabric::HelloMultiplier>,
+    #[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<FrrWord> 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 <ipv4-address>
 /// ```
-#[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 <area>`
@@ -156,10 +120,16 @@ impl Display for NetworkType {
 ///  ip ospf passive <value>
 ///  ip ospf network <value>
 /// ```
-#[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<bool>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
     pub network_type: Option<NetworkType>,
 }
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<u32>,
+    #[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<AccessListRule>,
 }
 
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct PrefixList {
+    pub name: PrefixListName,
+    pub rules: Vec<PrefixListRule>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct PrefixListRule {
+    pub action: AccessAction,
+    pub network: Cidr,
+    pub seq: Option<u32>,
+    pub le: Option<u32>,
+    pub ge: Option<u32>,
+    #[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 <ip-address>
 /// ```
-#[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 <ip-address>
 /// 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<RouteMapMatch>,
+    #[serde(default)]
     pub sets: Vec<RouteMapSet>,
-}
-
-/// 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 <protocol> route-map <route-map-name>
-/// ! or
-/// ipv6 protocol <protocol> route-map <route-map-name>
-/// ```
-#[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<String>,
 }
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<Vec<String>, 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<String, anyhow::Error> {
-    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<String, anyhow::Error> {
+    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<Vec<String>, 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<Net>;
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<T>::into_inner`].
 #[repr(transparent)]
-#[derive(Clone, Default, Debug)]
+#[derive(Clone, Default, Debug, Serialize, Deserialize)]
 pub struct Valid<T>(T);
 
 impl<T> Valid<T> {
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<FabricConfig>`].
 ///
 /// 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<FabricConfig>,
-    frr_config: &mut ser::FrrConfig,
+    frr_config: &mut FrrConfig,
 ) -> Result<(), anyhow::Error> {
     let mut routemap_seq = 100;
     let mut current_router_id: Option<Ipv4Addr> = 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<OspfRouter, anyhow::Error> {
+    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<OspfInterface>, 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<OspfInterface>, 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<OpenfabricInterface>, 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<OpenfabricInterface>, 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<ser::route_map::RouteMap, anyhow::Error> {
+) -> 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





  parent reply	other threads:[~2026-02-03 16:05 UTC|newest]

Thread overview: 24+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-02-03 16:01 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} 00/23] Generate frr config using jinja templates and rust types Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 1/9] ve-config: firewall: cargo fmt Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 2/9] frr: add proxmox-frr-templates package that contains templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 3/9] ve-config: remove FrrConfigBuilder struct Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 4/9] sdn-types: support variable-length NET identifier Gabriel Goller
2026-02-03 16:01 ` Gabriel Goller [this message]
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 6/9] frr: add isis configuration and templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 7/9] frr: support custom frr configuration lines Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 8/9] frr: add bgp support with templates and serialization Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 9/9] frr: store frr template content as a const map Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-perl-rs 1/2] sdn: add function to generate the frr config for all daemons Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-perl-rs 2/2] sdn: add method to get a frr template Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 01/10] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 02/10] sdn: tests: add missing comment " Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 03/10] tests: use Test::Differences to make test assertions Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 04/10] sdn: write structured frr config that can be rendered using templates Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 05/10] tests: rearrange some statements in the frr config Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 06/10] sdn: adjust frr.conf.local merging to rust template types Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 07/10] cli: add pvesdn cli tool for managing frr template overrides Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 08/10] debian: handle user modifications to FRR templates via ucf Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 09/10] api: add dry-run endpoint for sdn apply to preview changes Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 10/10] test: add test for frr.conf.local merging Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-manager 1/1] sdn: add dry-run view for sdn apply Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-docs 1/1] docs: add man page for the `pvesdn` cli Gabriel Goller

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260203160246.353351-6-g.goller@proxmox.com \
    --to=g.goller@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal