public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: Gabriel Goller <g.goller@proxmox.com>
Cc: pve-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox-ve-rs v6 05/20] frr: add template serializer and serialize fabrics using templates
Date: Thu, 19 Mar 2026 11:32:39 +0100	[thread overview]
Message-ID: <wn63pg6jpojcdn72s5oendxxszzrbqsnguodghmyazmkoe3d2f@ohyiewdhlgrh> (raw)
In-Reply-To: <20260312142732.370403-6-g.goller@proxmox.com>

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 <g.goller@proxmox.com>
> ---
>  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 <!nocheck>,
>   rustc:native (>= 1.82) <!nocheck>,
>   libstd-rust-dev <!nocheck>,
>   librust-anyhow-1+default-dev <!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-phf-0.11+default-dev (>= 0.11.2-~~) <!nocheck>,
> + librust-phf-0.11+macros-dev (>= 0.11.2-~~) <!nocheck>,
>   librust-proxmox-network-types-1+default-dev (>= 1.0.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 +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<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,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<T: AsRef<str> + Into<String>>(name: T) -> Result<Self, FrrWordError> {
>          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<str> for FrrWord {
>      fn as_ref(&self) -> &str {
>          &self.0
> @@ -157,7 +69,7 @@ impl AsRef<str> 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, Self::Error> {
> -        Self::new(value)
> +    fn try_from(s: &str) -> Result<Self, Self::Error> {
> +        Self::validate(s).map(Self::from_str_unchecked)
>      }
>  }
>  
> -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)
> +        if Self::validate(&value).is_ok() {
> +            Ok(Self::from_string_unchecked(value))
> +        } else {
> +            Err(InterfaceNameError::TooLong)
> +        }
>      }
>  }
>  
> -impl CommonInterfaceName {
> -    pub fn new<T: AsRef<str> + Into<String>>(s: T) -> Result<Self, CommonInterfaceNameError> {
> -        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<RouterName, Router>,
> -    pub interfaces: BTreeMap<InterfaceName, Interface>,
> -    pub access_lists: Vec<AccessList>,
> -    pub routemaps: Vec<RouteMap>,
> -    pub protocol_routemaps: BTreeSet<ProtocolRouteMap>,
> -}
> +#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)]
> +pub struct Interface<T> {
> +    // 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<Ipv4Cidr>,
> +    #[serde(default, skip_serializing_if = "Vec::is_empty")]
> +    pub addresses_v6: Vec<Ipv6Cidr>,
>  
> -impl FrrConfig {
> -    pub fn new() -> Self {
> -        Self::default()
> +    #[serde(flatten)]
> +    pub properties: T,
> +}
> +impl From<openfabric::OpenfabricInterface> for Interface<openfabric::OpenfabricInterface> {
> +    fn from(value: openfabric::OpenfabricInterface) -> Self {
> +        Interface {
> +            addresses_v4: Vec::new(),
> +            addresses_v6: Vec::new(),
> +            properties: value,
> +        }
>      }
> +}
>  
> -    pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
> -        self.router.iter()
> +impl From<ospf::OspfInterface> for Interface<ospf::OspfInterface> {
> +    fn from(value: ospf::OspfInterface) -> Self {
> +        Interface {
> +            addresses_v4: Vec::new(),
> +            addresses_v6: Vec::new(),
> +            properties: value,
> +        }
>      }
> +}
>  
> -    pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
> -        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<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 IpRoute {
> +    #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
> +    is_ipv6: bool,
> +    prefix: Cidr,
> +    via: IpOrInterface,
> +    vrf: Option<InterfaceName>,
> +}
>  
> -    pub fn protocol_routemaps(&self) -> impl Iterator<Item = &ProtocolRouteMap> + '_ {
> -        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<RouteMapName>,
> +    pub v6: Option<RouteMapName>,
> +}
> +
> +/// 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<FrrProtocol, IpProtocolRouteMap>,
> +    #[serde(default)]
> +    pub routemaps: BTreeMap<RouteMapName, Vec<RouteMapEntry>>,
> +    #[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..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<FrrWord> 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 <fabric_id>
> -///  ipv6 router openfabric <fabric_id>
> -///  openfabric hello-interval <value>
> -///  openfabric hello-multiplier <value>
> -///  openfabric csnp-interval <value>
> -///  openfabric passive <value>
> -/// ```
> +/// 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<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..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<FrrWord> 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 <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,
>  }
> @@ -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 <area>`
>  /// command.
> -///
> -/// This serializes to:
> -///
> -/// ```text
> -/// router ospf
> -///  ip ospf area <area>
> -///  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)]
>  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..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<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
>  /// 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<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,
> +}
> +
> +#[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 <access-list-name>
> -/// ! or
> -///  match ip next-hop <ip-address>
> -/// ! or
> -///  match ipv6 address <access-list-name>
> -/// ! 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),
> +    #[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 <name> permit 100
> -///  match ip address <access-list>
> -///  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..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<Vec<String>, 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<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)?,
> -        }
> -        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<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..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<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,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<OspfInterface>, 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<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 +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<OpenfabricInterface>, 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<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
> 
> 
> 
> 
> 




  reply	other threads:[~2026-03-19 10:32 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-03-12 14:26 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/20] Generate frr config using jinja templates and rust types Gabriel Goller
2026-03-12 14:26 ` [PATCH proxmox-ve-rs v6 01/20] ve-config: firewall: cargo fmt Gabriel Goller
2026-03-12 14:26 ` [PATCH proxmox-ve-rs v6 02/20] frr: add proxmox-frr-templates package that contains templates Gabriel Goller
2026-03-12 14:26 ` [PATCH proxmox-ve-rs v6 03/20] ve-config: remove FrrConfigBuilder struct Gabriel Goller
2026-03-12 14:26 ` [PATCH proxmox-ve-rs v6 04/20] sdn-types: support variable-length NET identifier Gabriel Goller
2026-03-12 14:26 ` [PATCH proxmox-ve-rs v6 05/20] frr: add template serializer and serialize fabrics using templates Gabriel Goller
2026-03-19 10:32   ` Wolfgang Bumiller [this message]
2026-03-12 14:26 ` [PATCH proxmox-ve-rs v6 06/20] frr: add isis configuration and templates Gabriel Goller
2026-03-12 14:26 ` [PATCH proxmox-ve-rs v6 07/20] frr: support custom frr configuration lines Gabriel Goller
2026-03-12 14:26 ` [PATCH proxmox-ve-rs v6 08/20] frr: add bgp support with templates and serialization Gabriel Goller
2026-03-12 14:26 ` [PATCH proxmox-ve-rs v6 09/20] frr: enable minijinja strict undefined behavior mode Gabriel Goller
2026-03-12 14:26 ` [PATCH proxmox-perl-rs v6 10/20] sdn: add function to generate the frr config for all daemons Gabriel Goller
2026-03-12 14:26 ` [PATCH pve-network v6 11/20] tests: use Test::Differences to make test assertions Gabriel Goller
2026-03-12 14:26 ` [PATCH pve-network v6 12/20] test: add tests for frr.conf.local merging Gabriel Goller
2026-03-12 14:26 ` [PATCH pve-network v6 13/20] test: bgp: add some various integration tests Gabriel Goller
2026-03-12 14:26 ` [PATCH pve-network v6 14/20] sdn: write structured frr config that can be rendered using templates Gabriel Goller
2026-03-12 14:26 ` [PATCH pve-network v6 15/20] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
2026-03-12 14:26 ` [PATCH pve-network v6 16/20] tests: rearrange some statements in the " Gabriel Goller
2026-03-12 14:26 ` [PATCH pve-network v6 17/20] sdn: adjust frr.conf.local merging to rust template types Gabriel Goller
2026-03-12 14:26 ` [PATCH pve-network v6 18/20] test: adjust frr_local_merge test for new template generation Gabriel Goller
2026-03-12 14:26 ` [PATCH pve-network v6 19/20] api: add dry-run endpoint for sdn apply to preview changes Gabriel Goller
2026-03-12 14:26 ` [PATCH pve-manager v6 20/20] sdn: add dry-run diff view for sdn apply Gabriel Goller
2026-03-12 15:42 ` [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/20] Generate frr config using jinja templates and rust types Stefan Hanreich
2026-03-18  9:29 ` Hannes Laimer
2026-03-18 17:50 ` Stefan Hanreich

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=wn63pg6jpojcdn72s5oendxxszzrbqsnguodghmyazmkoe3d2f@ohyiewdhlgrh \
    --to=w.bumiller@proxmox.com \
    --cc=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