public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [RFC network/ve-rs 0/8] Template-based FRR config generation
@ 2025-09-19  9:41 Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 1/4] frr: add templates and structs to represent the frr config Gabriel Goller
                   ` (7 more replies)
  0 siblings, 8 replies; 9+ messages in thread
From: Gabriel Goller @ 2025-09-19  9:41 UTC (permalink / raw)
  To: pve-devel

Currently we generate the frr config by having a few rust-structs that look
very similar to the literal frr config -- these structs then implement a custom
trait similar to Display, which serializes the config into a String. This is
not optimal due to multiple reasons, the main one being it's quite verbose and
and the fact that we often mixed Display and FrrDump implementations. This
means that sometimes structs are serialized using Display and sometimes using
FrrDump. There is also the question of granularity, so does a single line
correspond to a single FrrDump implementation? Or does every frr word
correspond to an FrrDump implementation and a random one just adds a '
'?

In order to clean this up a bit, there was an attempt at writing a full-fledged
serde serializer, which takes structs and converts them into blocks and using
nested enums as the options on every line. This turned out to be quite
difficult and involved a lot of magic when creating the config in rust.
Especially the ending statements (e.g. 'exit-address-family' when exiting an
address-family block, but 'exit' when exiting a router block) turned out to be
quite tricky.

The third and probably most popular way (Vyos and Sonic both use jinja
templates as well) to generate frr config is to use templates. So you basically
have rust-structs that look similar to the frr config and implement derive or
implemennt serialize. Then we can use template files, which contain the whole
configuration with every option wrapped in a `if` block and lists/blocks
wrapped in `for` loops. Finally, we can use the rust structs to insert them into
the templates and get the full frr config. The major downside of this approach
is that the frr template files get quite complicated and can be a major source
of config generation errors. A positive thing though, is that we don't need to
model the whole frr config with every option and every property, we can easily
wrap a few options into a single (e.g.) bool, and then maybe split it out later
when we add a ui option for each property.
Here I made the decision to split every protocol into its own template file.
These template files then import some common structs, such as the interfaces and
route-maps, which are used in every protocol. To accommodate for this, I also had
to change the structure of the FrrConfig:
So we no longer have:

config/
├─ interfaces/
│  ├─ openfabric
│  ├─ ospf
├─ routers/
│  ├─ openfabric
│  ├─ ospf

but:

config/
├─ openfabric/
│  ├─ interfaces
│  ├─ routers
├─ ospf/
│  ├─ interfaces
│  ├─ routers

Which plays much nicer with the templates, and IMO still makes sense logically.

I used minijinja for this series, as that seems to be the most used template
engine in the rust ecosystem, even though we mostly seem to use handlebars. I
discussed this we Lukas and Thomas, and we'll decide in the next few weeks if
this is fine, or it's better if we use the same templating engine
everywhere.

The current frr.conf.local is quite broken, so in order to introduce a new and
(a bit) saner option to write custom frr config, add a directory where the user
can specific custom template files to customize the frr config generated by the
sdn stack. Having a templating engine is very powerful here, as the user could
write something like this:

    interface {{ interface_name }}
    {% if interface_name == "ens19" %}
     ip network point-to-point
    {% endif %}

Obviously if you don't know what you're doing, you can break everything, so I'd
wouldn't advertise this feature too much :)

The pve-network patches are only here to make the generated frr config nicer
(removing duplicated "!" comments) and fixing/improving the tests.

ve-rs:

Gabriel Goller (4):
  frr: add templates and structs to represent the frr config
  sdn-types: forward serialize to display for NET
  ve-config: fabrics: use new proxmox-frr structs to generate frr config
  tests: always prepend the frr delimiter/comment "!" to the block

 proxmox-frr/Cargo.toml                        |   4 +
 proxmox-frr/debian/control                    |  14 +
 proxmox-frr/examples/route_map.rs             |  70 +++++
 proxmox-frr/src/lib.rs                        | 166 ++++--------
 proxmox-frr/src/openfabric.rs                 |  15 +-
 proxmox-frr/src/ospf.rs                       |  37 +--
 proxmox-frr/src/route_map.rs                  | 108 ++------
 proxmox-frr/src/serializer.rs                 | 256 +++++-------------
 proxmox-frr/templates/access_list.jinja       |   6 +
 proxmox-frr/templates/fabricd.jinja           |  36 +++
 proxmox-frr/templates/interface.jinja         |   4 +
 proxmox-frr/templates/ospfd.jinja             |  27 ++
 proxmox-frr/templates/route_map.jinja         |  11 +
 proxmox-sdn-types/src/net.rs                  |   4 +-
 proxmox-ve-config/src/sdn/fabric/frr.rs       | 145 +++++-----
 proxmox-ve-config/src/sdn/frr.rs              |  11 +-
 .../fabric__openfabric_default_pve.snap       |   2 +-
 .../fabric__openfabric_default_pve1.snap      |   2 +-
 .../fabric__openfabric_dualstack_pve.snap     |   8 +-
 .../fabric__openfabric_ipv6_only_pve.snap     |   2 +-
 .../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 +-
 24 files changed, 426 insertions(+), 510 deletions(-)
 create mode 100644 proxmox-frr/examples/route_map.rs
 create mode 100644 proxmox-frr/templates/access_list.jinja
 create mode 100644 proxmox-frr/templates/fabricd.jinja
 create mode 100644 proxmox-frr/templates/interface.jinja
 create mode 100644 proxmox-frr/templates/ospfd.jinja
 create mode 100644 proxmox-frr/templates/route_map.jinja


network:

Gabriel Goller (4):
  sdn: remove duplicate comment line '!' in frr config
  sdn: add trailing newline in frr config
  sdn: tests: add missing comment '!' in frr config
  tests: use Test::Differences to make test assertions

 debian/control                                |  1 +
 src/PVE/Network/SDN/Frr.pm                    |  3 +-
 src/test/run_test_dns.pl                      | 15 ++++-----
 src/test/run_test_ipams.pl                    | 13 ++++----
 src/test/run_test_subnets.pl                  | 31 ++++++++++---------
 src/test/run_test_vnets_blackbox.pl           | 17 +++++-----
 src/test/run_test_zones.pl                    |  5 +--
 .../expected_controller_config                |  3 +-
 .../expected_controller_config                |  3 +-
 .../evpn/ebgp/expected_controller_config      |  3 +-
 .../ebgp_loopback/expected_controller_config  |  3 +-
 .../evpn/exitnode/expected_controller_config  |  3 +-
 .../expected_controller_config                |  3 +-
 .../expected_controller_config                |  3 +-
 .../exitnode_snat/expected_controller_config  |  3 +-
 .../expected_controller_config                |  3 +-
 .../evpn/ipv4/expected_controller_config      |  3 +-
 .../evpn/ipv4ipv6/expected_controller_config  |  3 +-
 .../expected_controller_config                |  3 +-
 .../evpn/ipv6/expected_controller_config      |  3 +-
 .../ipv6underlay/expected_controller_config   |  3 +-
 .../evpn/isis/expected_controller_config      |  3 +-
 .../isis_loopback/expected_controller_config  |  3 +-
 .../expected_controller_config                |  3 +-
 .../expected_controller_config                |  3 +-
 .../multiplezones/expected_controller_config  |  3 +-
 .../expected_controller_config                |  5 ++-
 .../ospf_fabric/expected_controller_config    |  5 ++-
 .../evpn/rt_import/expected_controller_config |  3 +-
 .../evpn/vxlanport/expected_controller_config |  3 +-
 30 files changed, 70 insertions(+), 88 deletions(-)


Summary over all repositories:
  54 files changed, 496 insertions(+), 598 deletions(-)

-- 
Generated by git-murpp 0.8.0


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [pve-devel] [PATCH ve-rs 1/4] frr: add templates and structs to represent the frr config
  2025-09-19  9:41 [pve-devel] [RFC network/ve-rs 0/8] Template-based FRR config generation Gabriel Goller
@ 2025-09-19  9:41 ` Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 2/4] sdn-types: forward serialize to display for NET Gabriel Goller
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Gabriel Goller @ 2025-09-19  9:41 UTC (permalink / raw)
  To: pve-devel

Add jinja templates and the minijinja configuration + structs to
represent the frr config in rust. The rust config can then be rendered
using minijinja and the jinja template files.

Serializing the config using templates provides better ergonomics and is
cleaner than writing an overly complex serde serializer for the
convoluted frr config format. It's also better than the current model,
where we have a custom trait similar to Display that serializes structs.
The current approach has a few problem with the most prominent being the
difference between Display and the custom serializer trait -- so we
some times serialize using Display and some using FrrDumper.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-frr/Cargo.toml                  |   4 +
 proxmox-frr/debian/control              |  14 ++
 proxmox-frr/examples/route_map.rs       |  70 +++++++
 proxmox-frr/src/lib.rs                  | 166 +++++----------
 proxmox-frr/src/openfabric.rs           |  15 +-
 proxmox-frr/src/ospf.rs                 |  37 +---
 proxmox-frr/src/route_map.rs            | 108 +++-------
 proxmox-frr/src/serializer.rs           | 256 +++++++-----------------
 proxmox-frr/templates/access_list.jinja |   6 +
 proxmox-frr/templates/fabricd.jinja     |  36 ++++
 proxmox-frr/templates/interface.jinja   |   4 +
 proxmox-frr/templates/ospfd.jinja       |  27 +++
 proxmox-frr/templates/route_map.jinja   |  11 +
 13 files changed, 331 insertions(+), 423 deletions(-)
 create mode 100644 proxmox-frr/examples/route_map.rs
 create mode 100644 proxmox-frr/templates/access_list.jinja
 create mode 100644 proxmox-frr/templates/fabricd.jinja
 create mode 100644 proxmox-frr/templates/interface.jinja
 create mode 100644 proxmox-frr/templates/ospfd.jinja
 create mode 100644 proxmox-frr/templates/route_map.jinja

diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
index 8b01fa4bf806..a14eab20a462 100644
--- a/proxmox-frr/Cargo.toml
+++ b/proxmox-frr/Cargo.toml
@@ -13,6 +13,10 @@ rust-version.workspace = true
 thiserror = { workspace = true }
 anyhow = "1"
 tracing = "0.1"
+serde = { workspace = true, features = [ "derive" ] }
+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 999661978f90..2c31b52d0408 100644
--- a/proxmox-frr/debian/control
+++ b/proxmox-frr/debian/control
@@ -7,8 +7,15 @@ 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-thiserror-2+default-dev <!nocheck>,
  librust-tracing-0.1+default-dev <!nocheck>
 Maintainer: Proxmox Support Team <support@proxmox.com>
@@ -25,8 +32,15 @@ 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-thiserror-2+default-dev,
  librust-tracing-0.1+default-dev
 Provides:
diff --git a/proxmox-frr/examples/route_map.rs b/proxmox-frr/examples/route_map.rs
new file mode 100644
index 000000000000..9dc641caaf89
--- /dev/null
+++ b/proxmox-frr/examples/route_map.rs
@@ -0,0 +1,70 @@
+use std::collections::{BTreeMap, BTreeSet};
+
+use proxmox_frr::{
+    openfabric::{OpenfabricInterface, OpenfabricRouterName},
+    route_map::{
+        AccessAction, AccessListName, RouteMap, RouteMapMatch, RouteMapMatchInner, RouteMapName,
+        RouteMapSet,
+    },
+    serializer::to_raw_config,
+    FrrConfig, Interface, InterfaceName, OpenfabricFrrConfig,
+};
+
+fn main() {
+    let mut interfaces = BTreeMap::new();
+    interfaces.insert(
+        InterfaceName::new("ens18").unwrap(),
+        Interface {
+            addresses: vec![
+                "6.6.6.4/32".parse().unwrap(),
+                "10.10.10.0/24".parse().unwrap(),
+            ],
+            properties: OpenfabricInterface::builder()
+                .fabric_id(OpenfabricRouterName::new("cool".parse().unwrap()))
+                .passive(true)
+                .is_ipv4(true)
+                .is_ipv6(false)
+                .build(),
+        },
+    );
+    interfaces.insert(
+        InterfaceName::new("ens19").unwrap(),
+        Interface {
+            addresses: vec!["6.6.6.5/32".parse().unwrap()],
+            properties: OpenfabricInterface::builder()
+                .fabric_id(OpenfabricRouterName::new("cool".parse().unwrap()))
+                .is_ipv4(false)
+                .is_ipv6(false)
+                .build(),
+        },
+    );
+    let config = OpenfabricFrrConfig {
+        router: BTreeMap::new(),
+        interfaces,
+        access_lists: Vec::new(),
+        protocol_routemaps: BTreeSet::new(),
+        routemaps: vec![RouteMap {
+            name: RouteMapName::new("SOMETHING".to_string()),
+            seq: 10,
+            action: AccessAction::Permit,
+            matches: vec![
+                RouteMapMatch::V4(RouteMapMatchInner::Address(AccessListName::new(
+                    "coolList".to_string(),
+                ))),
+                RouteMapMatch::V4(RouteMapMatchInner::NextHop("127.0.0.1".to_string())),
+                RouteMapMatch::V6(RouteMapMatchInner::NextHop("127.0.0.1".to_string())),
+            ],
+            sets: vec![
+                RouteMapSet::Src("8.8.8.8".parse().unwrap()),
+                RouteMapSet::Community("coolCommunity".to_string()),
+            ],
+        }],
+    };
+
+    let full_config = FrrConfig {
+        openfabric: config,
+        ospf: proxmox_frr::OspfFrrConfig::default(),
+    };
+    let rendered = to_raw_config(&full_config).expect("error rendering frr config");
+    println!("{}", rendered.join("\n"));
+}
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index 86101182fafd..d255b81e1f40 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -4,11 +4,14 @@ pub mod route_map;
 pub mod serializer;
 
 use std::collections::{BTreeMap, BTreeSet};
-use std::fmt::Display;
 use std::str::FromStr;
 
 use crate::route_map::{AccessList, ProtocolRouteMap, RouteMap};
 
+use openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
+use ospf::{OspfInterface, OspfRouter, OspfRouterName};
+use proxmox_network_types::ip_address::Cidr;
+use serde::Serialize;
 use thiserror::Error;
 
 /// Generic FRR router.
@@ -34,73 +37,6 @@ impl From<openfabric::OpenfabricRouter> for Router {
     }
 }
 
-/// 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 +49,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)]
 pub struct FrrWord(String);
 
 impl FrrWord {
@@ -144,12 +80,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 +87,7 @@ impl AsRef<str> for FrrWord {
 }
 
 #[derive(Error, Debug)]
-pub enum CommonInterfaceNameError {
+pub enum InterfaceNameError {
     #[error("interface name too long")]
     TooLong,
 }
@@ -166,38 +96,58 @@ 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)]
+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)
+        }
+    }
+}
+
+#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+pub struct Interface<T> {
+    pub addresses: Vec<Cidr>,
+
+    #[serde(flatten)]
+    pub properties: T,
+}
+
+impl From<OpenfabricInterface> for Interface<OpenfabricInterface> {
+    fn from(value: OpenfabricInterface) -> Self {
+        Interface {
+            addresses: Vec::new(),
+            properties: value,
         }
     }
 }
 
-impl Display for CommonInterfaceName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.fmt(f)
+impl From<OspfInterface> for Interface<OspfInterface> {
+    fn from(value: OspfInterface) -> Self {
+        Interface {
+            addresses: Vec::new(),
+            properties: value,
+        }
     }
 }
 
@@ -208,34 +158,24 @@ impl Display for CommonInterfaceName {
 /// 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 openfabric: OpenfabricFrrConfig,
+    pub ospf: OspfFrrConfig,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize)]
+pub struct OpenfabricFrrConfig {
+    pub router: BTreeMap<OpenfabricRouterName, OpenfabricRouter>,
+    pub interfaces: BTreeMap<InterfaceName, Interface<OpenfabricInterface>>,
     pub access_lists: Vec<AccessList>,
     pub routemaps: Vec<RouteMap>,
     pub protocol_routemaps: BTreeSet<ProtocolRouteMap>,
 }
 
-impl FrrConfig {
-    pub fn new() -> Self {
-        Self::default()
-    }
-
-    pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
-        self.router.iter()
-    }
-
-    pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
-        self.interfaces.iter()
-    }
-
-    pub fn access_lists(&self) -> impl Iterator<Item = &AccessList> + '_ {
-        self.access_lists.iter()
-    }
-    pub fn routemaps(&self) -> impl Iterator<Item = &RouteMap> + '_ {
-        self.routemaps.iter()
-    }
-
-    pub fn protocol_routemaps(&self) -> impl Iterator<Item = &ProtocolRouteMap> + '_ {
-        self.protocol_routemaps.iter()
-    }
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize)]
+pub struct OspfFrrConfig {
+    pub router: BTreeMap<OspfRouterName, OspfRouter>,
+    pub interfaces: BTreeMap<InterfaceName, Interface<OspfInterface>>,
+    pub access_lists: Vec<AccessList>,
+    pub routemaps: Vec<RouteMap>,
+    pub protocol_routemaps: BTreeSet<ProtocolRouteMap>,
 }
diff --git a/proxmox-frr/src/openfabric.rs b/proxmox-frr/src/openfabric.rs
index 6e2a7200ab37..8f3386416137 100644
--- a/proxmox-frr/src/openfabric.rs
+++ b/proxmox-frr/src/openfabric.rs
@@ -1,15 +1,16 @@
 use std::fmt::Debug;
-use std::fmt::Display;
 
+use bon::Builder;
 use proxmox_sdn_types::net::Net;
 
+use serde::Serialize;
 use thiserror::Error;
 
 use crate::FrrWord;
 use crate::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)]
 pub struct OpenfabricRouterName(FrrWord);
 
 impl From<FrrWord> for OpenfabricRouterName {
@@ -24,16 +25,10 @@ impl OpenfabricRouterName {
     }
 }
 
-impl Display for OpenfabricRouterName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "openfabric {}", self.0)
-    }
-}
-
 /// 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)]
 pub struct OpenfabricRouter {
     /// The NET address
     pub net: Net,
@@ -67,7 +62,7 @@ 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, 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,
diff --git a/proxmox-frr/src/ospf.rs b/proxmox-frr/src/ospf.rs
index d0e098e099d2..ce67b9158047 100644
--- a/proxmox-frr/src/ospf.rs
+++ b/proxmox-frr/src/ospf.rs
@@ -1,7 +1,8 @@
 use std::fmt::Debug;
-use std::fmt::Display;
 use std::net::Ipv4Addr;
 
+use bon::Builder;
+use serde::Serialize;
 use thiserror::Error;
 
 use crate::{FrrWord, FrrWordError};
@@ -18,15 +19,9 @@ use crate::{FrrWord, FrrWordError};
 /// router ospf
 /// !...
 /// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
 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 +39,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)]
 pub struct Area(FrrWord);
 
 impl TryFrom<FrrWord> for Area {
@@ -65,12 +60,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 +73,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)]
 pub struct OspfRouter {
     pub router_id: Ipv4Addr,
 }
@@ -119,7 +108,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)]
+#[serde(rename_all = "kebab-case")]
 pub enum NetworkType {
     Broadcast,
     NonBroadcast,
@@ -132,17 +122,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,7 +135,7 @@ 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, Builder)]
 pub struct OspfInterface {
     // Note: an interface can only be a part of a single area(so no vec needed here)
     pub area: Area,
diff --git a/proxmox-frr/src/route_map.rs b/proxmox-frr/src/route_map.rs
index 0918a3cead14..2ef9d75618a5 100644
--- a/proxmox-frr/src/route_map.rs
+++ b/proxmox-frr/src/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::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, Serialize)]
+#[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,15 +30,16 @@ 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)]
 pub struct AccessListRule {
     pub action: AccessAction,
     pub network: Cidr,
     pub seq: Option<u32>,
+    pub is_ipv6: bool,
 }
 
 /// The name of an [`AccessList`].
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
 pub struct AccessListName(String);
 
 impl AccessListName {
@@ -57,12 +48,6 @@ impl AccessListName {
     }
 }
 
-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,7 +60,7 @@ 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)]
 pub struct AccessList {
     pub name: AccessListName,
     pub rules: Vec<AccessListRule>,
@@ -98,66 +83,38 @@ pub struct AccessList {
 /// ! or
 ///  match ipv6 next-hop <ip-address>
 /// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
+#[serde(tag = "protocol_type", rename_all = "lowercase")]
 pub enum RouteMapMatch {
+    #[serde(rename = "ip")]
     V4(RouteMapMatchInner),
+    #[serde(rename = "ipv6")]
     V6(RouteMapMatchInner),
 }
 
-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}")
-                }
-            },
-        }
-    }
-}
-
 /// A route-map match statement generic on the IP-version.
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
+#[serde(tag = "match_type", content = "value", rename_all = "kebab-case")]
 pub enum RouteMapMatchInner {
-    IpAddress(AccessListName),
-    IpNextHop(String),
+    Address(AccessListName),
+    NextHop(String),
 }
 
 /// 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)]
+#[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)]
+/// Name of the route-map
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
 pub struct RouteMapName(String);
 
 impl RouteMapName {
@@ -166,12 +123,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,7 +137,7 @@ impl Display for RouteMapName {
 ///  set src <ip-address>
 /// exit
 /// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
 pub struct RouteMap {
     pub name: RouteMapName,
     pub seq: u32,
@@ -198,21 +149,13 @@ pub struct RouteMap {
 /// The ProtocolType used in the [`ProtocolRouteMap`].
 ///
 /// Specifies to which protocols we can attach route-maps.
-#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
+#[serde(rename_all = "kebab-case")]
 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
@@ -225,9 +168,8 @@ impl Display for ProtocolType {
 /// ! or
 /// ipv6 protocol <protocol> route-map <route-map-name>
 /// ```
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
 pub struct ProtocolRouteMap {
     pub is_ipv6: bool,
-    pub protocol: ProtocolType,
     pub routemap_name: RouteMapName,
 }
diff --git a/proxmox-frr/src/serializer.rs b/proxmox-frr/src/serializer.rs
index f8a3c7238d94..ade4a2aae7ca 100644
--- a/proxmox-frr/src/serializer.rs
+++ b/proxmox-frr/src/serializer.rs
@@ -1,203 +1,83 @@
-use std::fmt::{self, Write};
-
-use crate::{
-    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),
-}
+use std::{fs, path::PathBuf};
+
+use minijinja::Environment;
+
+use crate::{FrrConfig, OpenfabricFrrConfig, OspfFrrConfig};
+
+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/pve/sdn/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!("../templates/fabricd.jinja").to_owned())),
+                "ospfd.jinja" => Ok(Some(include_str!("../templates/ospfd.jinja").to_owned())),
+                "route_map.jinja" => Ok(Some(
+                    include_str!("../templates/route_map.jinja").to_owned(),
+                )),
+                "interface.jinja" => Ok(Some(
+                    include_str!("../templates/interface.jinja").to_owned(),
+                )),
+                "access_list.jinja" => Ok(Some(
+                    include_str!("../templates/access_list.jinja").to_owned(),
+                )),
+                _ => Ok(None),
+            },
+        }
+    });
 
-impl Write for FrrConfigBlob<'_> {
-    fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
-        self.buf.write_str(s)
-    }
+    env
 }
 
-pub trait FrrSerializer {
-    fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result;
+fn render_openfabric_config(
+    env: &mut Environment<'_>,
+    config: &OpenfabricFrrConfig,
+) -> Result<String, anyhow::Error> {
+    let template = env.get_template("fabricd.jinja")?;
+    let rendered = template.render(config)?;
+    Ok(rendered)
 }
 
-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())
+fn render_ospf_config(
+    env: &mut Environment<'_>,
+    config: &OspfFrrConfig,
+) -> Result<String, anyhow::Error> {
+    let template = env.get_template("ospfd.jinja")?;
+    let rendered = template.render(config)?;
+    Ok(rendered)
 }
 
+/// 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(())
-    }
-}
+    let mut result = String::new();
+    let mut env = create_env();
 
-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(())
-    }
-}
+    result.push_str(&render_openfabric_config(&mut env, &config.openfabric)?);
+    result.push_str(&render_ospf_config(&mut env, &config.ospf)?);
 
-impl FrrSerializer for (&AccessListName, &AccessList) {
-    fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
-        self.1.serialize(f)?;
-        writeln!(f, "!")
-    }
+    Ok(result)
 }
 
-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(())
-    }
-}
+/// 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> {
+    let mut result = String::new();
+    let mut env = create_env();
 
-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(())
-    }
-}
+    result.push_str(&render_openfabric_config(&mut env, &config.openfabric)?);
+    result.push_str(&render_ospf_config(&mut env, &config.ospf)?);
 
-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(())
-    }
+    Ok(result
+        .lines()
+        .map(|line| line.to_owned())
+        .collect::<Vec<String>>())
 }
diff --git a/proxmox-frr/templates/access_list.jinja b/proxmox-frr/templates/access_list.jinja
new file mode 100644
index 000000000000..2faf467ff166
--- /dev/null
+++ b/proxmox-frr/templates/access_list.jinja
@@ -0,0 +1,6 @@
+{% for access_list in access_lists %}
+!
+{%  for rule in access_list.rules %}
+{{ "ipv6 " if rule.is_ipv6 }}access-list {{ access_list.name }} {{ ("seq " ~ rules.seq ~ " ") if rule.seq }}{{ rule.action }} {{ rule.network }}
+{%  endfor%}
+{% endfor %}
diff --git a/proxmox-frr/templates/fabricd.jinja b/proxmox-frr/templates/fabricd.jinja
new file mode 100644
index 000000000000..e7fe511da190
--- /dev/null
+++ b/proxmox-frr/templates/fabricd.jinja
@@ -0,0 +1,36 @@
+{% for router_name, router_config in router|items %}
+!
+router openfabric {{ router_name }}
+ net {{ router_config.net }}
+exit
+{% endfor %}
+{% for interface_name, interface_config in interfaces|items %}
+!
+{% include 'interface.jinja' %}
+{% if interface_config.fabric_id and interface_config.is_ipv4 %}
+ ip router openfabric {{ interface_config.fabric_id }}
+{% endif %}
+{% if interface_config.fabric_id and interface_config.is_ipv6 %}
+ ipv6 router openfabric {{ interface_config.fabric_id }}
+{% endif %}
+{% if interface_config.passive %}
+ openfabric passive
+{% endif %}
+{% if interface_config.hello_interval %}
+ openfabric hello-interval {{ interface_config.hello_interval}}
+{% endif %}
+{% if interface_config.hello_multiplier %}
+ openfabric hello-multiplier {{ interface_config.hello_multiplier}}
+{% endif %}
+{% if interface_config.csnp_interval %}
+ openfabric csnp-interval {{ interface_config.csnp_interval}}
+{% endif %}
+exit
+{% endfor %}
+{% include 'access_list.jinja' %}
+{% include 'route_map.jinja' %}
+{% for protocol_routemap in protocol_routemaps %}
+!
+{{ "ipv6" if protocol_routemap.is_ipv6 else "ip"}} protocol openfabric route-map {{ protocol_routemap.routemap_name }}
+{% endfor %}
+
diff --git a/proxmox-frr/templates/interface.jinja b/proxmox-frr/templates/interface.jinja
new file mode 100644
index 000000000000..f4a87e24fc41
--- /dev/null
+++ b/proxmox-frr/templates/interface.jinja
@@ -0,0 +1,4 @@
+interface {{ interface_name }}
+{% for address in interface_config.addresses %}
+ ip address {{address}}
+{% endfor %}
diff --git a/proxmox-frr/templates/ospfd.jinja b/proxmox-frr/templates/ospfd.jinja
new file mode 100644
index 000000000000..de9aceb4fc6a
--- /dev/null
+++ b/proxmox-frr/templates/ospfd.jinja
@@ -0,0 +1,27 @@
+{% for router_name, router_config in router|items %}
+{%  if loop.first is true %}
+!
+router ospf
+ ospf router-id {{ router_config.router_id }}
+exit
+{%  endif %}
+{% endfor %}
+{% for interface_name, interface_config in interfaces|items %}
+!
+{% include 'interface.jinja' %}
+ ip ospf area {{ interface_config.area }}
+{% if interface_config.passive %}
+ ip ospf passive
+{% endif %}
+{% if interface_config.network_type %}
+ ip ospf network {{ interface_config.network_type }}
+{% endif %}
+exit
+{% endfor %}
+{% include 'access_list.jinja' %}
+{% include 'route_map.jinja' %}
+{% for protocol_routemap in protocol_routemaps %}
+!
+{{ "ipv6" if protocol_routemap.is_ipv6 else "ip"}} protocol ospf route-map {{ protocol_routemap.routemap_name }}
+{% endfor %}
+
diff --git a/proxmox-frr/templates/route_map.jinja b/proxmox-frr/templates/route_map.jinja
new file mode 100644
index 000000000000..1fd8c7197003
--- /dev/null
+++ b/proxmox-frr/templates/route_map.jinja
@@ -0,0 +1,11 @@
+{% for routemap in routemaps %}
+!
+route-map {{ routemap.name }} {{ routemap.action }} {{ routemap.seq }}
+{%  for match in routemap.matches %}
+ match {{ match.protocol_type }} {{ match.match_type }} {{ match.value }}
+{%  endfor %}
+{%  for set in routemap.sets %}
+ set {{ set.set_type }} {{ set.value }}
+{%  endfor %}
+exit
+{% endfor %}
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [pve-devel] [PATCH ve-rs 2/4] sdn-types: forward serialize to display for NET
  2025-09-19  9:41 [pve-devel] [RFC network/ve-rs 0/8] Template-based FRR config generation Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 1/4] frr: add templates and structs to represent the frr config Gabriel Goller
@ 2025-09-19  9:41 ` Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 3/4] ve-config: fabrics: use new proxmox-frr structs to generate frr config Gabriel Goller
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Gabriel Goller @ 2025-09-19  9:41 UTC (permalink / raw)
  To: pve-devel

The NET (Network Entity Title) is serialized by calling it's Display
implementation, which pretty-prints the whole NET as a single string.
This is needed because minijinja calls Serialize on the template fields.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-sdn-types/src/net.rs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/proxmox-sdn-types/src/net.rs b/proxmox-sdn-types/src/net.rs
index 3cd1e4f80ed7..6207cc3dcbc4 100644
--- a/proxmox-sdn-types/src/net.rs
+++ b/proxmox-sdn-types/src/net.rs
@@ -138,7 +138,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, Deserialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
 pub struct Net {
     afi: NetAFI,
     area: NetArea,
@@ -146,6 +146,8 @@ pub struct Net {
     selector: NetSelector,
 }
 
+proxmox_serde::forward_serialize_to_display!(Net);
+
 impl UpdaterType for Net {
     type Updater = Option<Net>;
 }
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [pve-devel] [PATCH ve-rs 3/4] ve-config: fabrics: use new proxmox-frr structs to generate frr config
  2025-09-19  9:41 [pve-devel] [RFC network/ve-rs 0/8] Template-based FRR config generation Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 1/4] frr: add templates and structs to represent the frr config Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 2/4] sdn-types: forward serialize to display for NET Gabriel Goller
@ 2025-09-19  9:41 ` Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 4/4] tests: always prepend the frr delimiter/comment "!" to the block Gabriel Goller
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Gabriel Goller @ 2025-09-19  9:41 UTC (permalink / raw)
  To: pve-devel

The proxmox-frr FrrConfig structs changed a bit so that we can serialize
them nicely using templates -- this means we also need to change the
FrrConfig generation code a bit.

Due to the decision that every protocol corresponds to a single template
file (with a few exceptions, e.g. common interface options etc.) we also
have per-protocol structs that contain all the structs needed for that
fabric to work.

So we no longer have:

config/
├─ interfaces/
│  ├─ openfabric
│  ├─ ospf
├─ routers/
│  ├─ openfabric
│  ├─ ospf

but:

config/
├─ openfabric/
│  ├─ interfaces
│  ├─ routers
├─ ospf/
│  ├─ interfaces
│  ├─ routers

Thought honestly I don't think this change is so impacting -- except a
few enums that we can remove.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-ve-config/src/sdn/fabric/frr.rs | 145 +++++++++++++-----------
 proxmox-ve-config/src/sdn/frr.rs        |  11 +-
 2 files changed, 81 insertions(+), 75 deletions(-)

diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index 486f7dc51dcb..86c77b8b4c57 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -1,17 +1,20 @@
 use std::net::{IpAddr, Ipv4Addr};
+
 use tracing;
 
-use proxmox_frr::ospf::{self, NetworkType};
+use proxmox_frr::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
+use proxmox_frr::ospf::{self, NetworkType, OspfInterface, OspfRouter, OspfRouterName};
 use proxmox_frr::route_map::{
-    AccessAction, AccessList, AccessListName, AccessListRule, ProtocolRouteMap, ProtocolType,
-    RouteMap, RouteMapMatch, RouteMapMatchInner, RouteMapName, RouteMapSet,
+    AccessAction, AccessList, AccessListName, AccessListRule, ProtocolRouteMap, RouteMap,
+    RouteMapMatch, RouteMapMatchInner, RouteMapName, RouteMapSet,
+};
+use proxmox_frr::{
+    FrrConfig, FrrWord, Interface, InterfaceName, OpenfabricFrrConfig, OspfFrrConfig,
 };
-use proxmox_frr::{FrrConfig, FrrWord, Interface, InterfaceName, Router, RouterName};
 use proxmox_network_types::ip_address::Cidr;
 use proxmox_sdn_types::net::Net;
 
 use crate::common::valid::Valid;
-
 use crate::sdn::fabric::section_config::protocol::{
     openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties},
     ospf::OspfInterfaceProperties,
@@ -31,6 +34,8 @@ pub fn build_fabric(
     let mut routemap_seq = 100;
     let mut current_router_id: Option<Ipv4Addr> = None;
     let mut current_net: Option<Net> = None;
+    let mut new_openfabric_config = OpenfabricFrrConfig::default();
+    let mut new_ospf_config = OspfFrrConfig::default();
 
     for (fabric_id, entry) in config.into_inner().iter() {
         match entry {
@@ -53,7 +58,9 @@ 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);
+                new_openfabric_config
+                    .router
+                    .insert(router_name, router_item);
 
                 // Create dummy interface for fabric
                 let (interface, interface_name) = build_openfabric_dummy_interface(
@@ -62,7 +69,7 @@ pub fn build_fabric(
                     node.ip6().is_some(),
                 )?;
 
-                if frr_config
+                if new_openfabric_config
                     .interfaces
                     .insert(interface_name, interface)
                     .is_some()
@@ -83,7 +90,7 @@ pub fn build_fabric(
                         node.ip6().is_some(),
                     )?;
 
-                    if frr_config
+                    if new_openfabric_config
                         .interfaces
                         .insert(interface_name, interface)
                         .is_some()
@@ -96,11 +103,12 @@ pub fn build_fabric(
                     let rule = AccessListRule {
                         action: AccessAction::Permit,
                         network: Cidr::from(ipv4cidr),
+                        is_ipv6: false,
                         seq: None,
                     };
                     let access_list_name =
                         AccessListName::new(format!("pve_openfabric_{}_ips", fabric_id));
-                    frr_config.access_lists.push(AccessList {
+                    new_openfabric_config.access_lists.push(AccessList {
                         name: access_list_name,
                         rules: vec![rule],
                     });
@@ -109,11 +117,12 @@ pub fn build_fabric(
                     let rule = AccessListRule {
                         action: AccessAction::Permit,
                         network: Cidr::from(ipv6cidr),
+                        is_ipv6: true,
                         seq: None,
                     };
                     let access_list_name =
                         AccessListName::new(format!("pve_openfabric_{}_ip6s", fabric_id));
-                    frr_config.access_lists.push(AccessList {
+                    new_openfabric_config.access_lists.push(AccessList {
                         name: access_list_name,
                         rules: vec![rule],
                     });
@@ -121,37 +130,43 @@ pub fn build_fabric(
 
                 if let Some(ipv4) = node.ip() {
                     // create route-map
-                    frr_config.routemaps.push(build_openfabric_routemap(
-                        fabric_id,
-                        IpAddr::V4(ipv4),
-                        routemap_seq,
-                    ));
+                    new_openfabric_config
+                        .routemaps
+                        .push(build_openfabric_routemap(
+                            fabric_id,
+                            IpAddr::V4(ipv4),
+                            routemap_seq,
+                        ));
                     routemap_seq += 10;
 
                     let protocol_routemap = ProtocolRouteMap {
                         is_ipv6: false,
-                        protocol: ProtocolType::Openfabric,
                         routemap_name: RouteMapName::new("pve_openfabric".to_owned()),
                     };
 
-                    frr_config.protocol_routemaps.insert(protocol_routemap);
+                    new_openfabric_config
+                        .protocol_routemaps
+                        .insert(protocol_routemap);
                 }
                 if let Some(ipv6) = node.ip6() {
                     // create route-map
-                    frr_config.routemaps.push(build_openfabric_routemap(
-                        fabric_id,
-                        IpAddr::V6(ipv6),
-                        routemap_seq,
-                    ));
+                    new_openfabric_config
+                        .routemaps
+                        .push(build_openfabric_routemap(
+                            fabric_id,
+                            IpAddr::V6(ipv6),
+                            routemap_seq,
+                        ));
                     routemap_seq += 10;
 
                     let protocol_routemap = ProtocolRouteMap {
                         is_ipv6: true,
-                        protocol: ProtocolType::Openfabric,
                         routemap_name: RouteMapName::new("pve_openfabric6".to_owned()),
                     };
 
-                    frr_config.protocol_routemaps.insert(protocol_routemap);
+                    new_openfabric_config
+                        .protocol_routemaps
+                        .insert(protocol_routemap);
                 }
             }
             FabricEntry::Ospf(ospf_entry) => {
@@ -167,13 +182,13 @@ pub fn build_fabric(
                 let frr_word_area = FrrWord::new(fabric.properties().area.to_string())?;
                 let frr_area = ospf::Area::new(frr_word_area)?;
                 let (router_name, router_item) = build_ospf_router(*router_id)?;
-                frr_config.router.insert(router_name, router_item);
+                new_ospf_config.router.insert(router_name, router_item);
 
                 // Add dummy interface
                 let (interface, interface_name) =
                     build_ospf_dummy_interface(fabric_id, frr_area.clone())?;
 
-                if frr_config
+                if new_ospf_config
                     .interfaces
                     .insert(interface_name, interface)
                     .is_some()
@@ -187,7 +202,7 @@ pub fn build_fabric(
                     let (interface, interface_name) =
                         build_ospf_interface(frr_area.clone(), interface)?;
 
-                    if frr_config
+                    if new_ospf_config
                         .interfaces
                         .insert(interface_name, interface)
                         .is_some()
@@ -203,10 +218,11 @@ 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(AccessList {
+                new_ospf_config.access_lists.push(AccessList {
                     name: access_list_name,
                     rules: vec![rule],
                 });
@@ -218,26 +234,26 @@ pub fn build_fabric(
                 )?;
 
                 routemap_seq += 10;
-                frr_config.routemaps.push(routemap);
+                new_ospf_config.routemaps.push(routemap);
 
                 let protocol_routemap = ProtocolRouteMap {
                     is_ipv6: false,
-                    protocol: ProtocolType::Ospf,
                     routemap_name: RouteMapName::new("pve_ospf".to_owned()),
                 };
 
-                frr_config.protocol_routemaps.insert(protocol_routemap);
+                new_ospf_config.protocol_routemaps.insert(protocol_routemap);
             }
         }
     }
+    frr_config.ospf = new_ospf_config;
+    frr_config.openfabric = new_openfabric_config;
     Ok(())
 }
 
 /// Helper that builds a OSPF router with a the router_id.
-fn build_ospf_router(router_id: Ipv4Addr) -> Result<(RouterName, Router), anyhow::Error> {
-    let ospf_router = proxmox_frr::ospf::OspfRouter { router_id };
-    let router_item = Router::Ospf(ospf_router);
-    let router_name = RouterName::Ospf(proxmox_frr::ospf::OspfRouterName);
+fn build_ospf_router(router_id: Ipv4Addr) -> Result<(OspfRouterName, OspfRouter), anyhow::Error> {
+    let router_item = proxmox_frr::ospf::OspfRouter { router_id };
+    let router_name = proxmox_frr::ospf::OspfRouterName;
     Ok((router_name, router_item))
 }
 
@@ -245,11 +261,10 @@ fn build_ospf_router(router_id: Ipv4Addr) -> Result<(RouterName, Router), anyhow
 fn build_openfabric_router(
     fabric_id: &FabricId,
     net: Net,
-) -> Result<(RouterName, Router), anyhow::Error> {
-    let ofr = proxmox_frr::openfabric::OpenfabricRouter { net };
-    let router_item = Router::Openfabric(ofr);
+) -> Result<(OpenfabricRouterName, OpenfabricRouter), anyhow::Error> {
+    let router_item = proxmox_frr::openfabric::OpenfabricRouter { net };
     let frr_word_id = FrrWord::new(fabric_id.to_string())?;
-    let router_name = RouterName::Openfabric(frr_word_id.into());
+    let router_name = frr_word_id.into();
     Ok((router_name, router_item))
 }
 
@@ -257,10 +272,10 @@ fn build_openfabric_router(
 fn build_ospf_interface(
     area: ospf::Area,
     interface: &OspfInterfaceProperties,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
+) -> Result<(Interface<OspfInterface>, InterfaceName), anyhow::Error> {
     let frr_interface = proxmox_frr::ospf::OspfInterface {
         area,
-        // Interfaces are always none-passive
+        // Interfaces are always non-passive
         passive: None,
         network_type: if interface.ip.is_some() {
             None
@@ -269,7 +284,7 @@ fn build_ospf_interface(
         },
     };
 
-    let interface_name = InterfaceName::Ospf(interface.name.as_str().try_into()?);
+    let interface_name = interface.name.as_str().try_into()?;
     Ok((frr_interface.into(), interface_name))
 }
 
@@ -277,13 +292,12 @@ fn build_ospf_interface(
 fn build_ospf_dummy_interface(
     fabric_id: &FabricId,
     area: ospf::Area,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
-    let frr_interface = proxmox_frr::ospf::OspfInterface {
-        area,
-        passive: Some(true),
-        network_type: None,
-    };
-    let interface_name = InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
+) -> Result<(Interface<OspfInterface>, InterfaceName), anyhow::Error> {
+    let frr_interface = proxmox_frr::ospf::OspfInterface::builder()
+        .area(area)
+        .passive(true)
+        .build();
+    let interface_name = format!("dummy_{}", fabric_id).try_into()?;
     Ok((frr_interface.into(), interface_name))
 }
 
@@ -297,7 +311,7 @@ fn build_openfabric_interface(
     fabric_config: &OpenfabricProperties,
     is_ipv4: bool,
     is_ipv6: bool,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
+) -> Result<(Interface<OpenfabricInterface>, InterfaceName), anyhow::Error> {
     let frr_word = FrrWord::new(fabric_id.to_string())?;
     let mut frr_interface = proxmox_frr::openfabric::OpenfabricInterface {
         fabric_id: frr_word.into(),
@@ -315,7 +329,7 @@ fn build_openfabric_interface(
     if frr_interface.hello_interval.is_none() {
         frr_interface.hello_interval = fabric_config.hello_interval;
     }
-    let interface_name = InterfaceName::Openfabric(interface.name.as_str().try_into()?);
+    let interface_name = interface.name.as_str().try_into()?;
     Ok((frr_interface.into(), interface_name))
 }
 
@@ -324,18 +338,15 @@ fn build_openfabric_dummy_interface(
     fabric_id: &FabricId,
     is_ipv4: bool,
     is_ipv6: bool,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
+) -> Result<(Interface<OpenfabricInterface>, InterfaceName), anyhow::Error> {
     let frr_word = FrrWord::new(fabric_id.to_string())?;
-    let frr_interface = proxmox_frr::openfabric::OpenfabricInterface {
-        fabric_id: frr_word.into(),
-        hello_interval: None,
-        passive: Some(true),
-        csnp_interval: None,
-        hello_multiplier: None,
-        is_ipv4,
-        is_ipv6,
-    };
-    let interface_name = InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
+    let frr_interface = proxmox_frr::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))
 }
 
@@ -350,14 +361,14 @@ fn build_openfabric_routemap(fabric_id: &FabricId, router_ip: IpAddr, seq: u32)
         seq,
         action: AccessAction::Permit,
         matches: vec![match router_ip {
-            IpAddr::V4(_) => RouteMapMatch::V4(RouteMapMatchInner::IpAddress(AccessListName::new(
+            IpAddr::V4(_) => RouteMapMatch::V4(RouteMapMatchInner::Address(AccessListName::new(
                 format!("pve_openfabric_{fabric_id}_ips"),
             ))),
-            IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::IpAddress(AccessListName::new(
+            IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::Address(AccessListName::new(
                 format!("pve_openfabric_{fabric_id}_ip6s"),
             ))),
         }],
-        sets: vec![RouteMapSet::IpSrc(router_ip)],
+        sets: vec![RouteMapSet::Src(router_ip)],
     }
 }
 
@@ -373,10 +384,10 @@ fn build_ospf_dummy_routemap(
         name: routemap_name.clone(),
         seq,
         action: AccessAction::Permit,
-        matches: vec![RouteMapMatch::V4(RouteMapMatchInner::IpAddress(
+        matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address(
             AccessListName::new(format!("pve_ospf_{fabric_id}_ips")),
         ))],
-        sets: vec![RouteMapSet::IpSrc(IpAddr::from(router_ip))],
+        sets: vec![RouteMapSet::Src(IpAddr::from(router_ip))],
     };
 
     Ok(routemap)
diff --git a/proxmox-ve-config/src/sdn/frr.rs b/proxmox-ve-config/src/sdn/frr.rs
index f7929c1f6c16..bc98440b9912 100644
--- a/proxmox-ve-config/src/sdn/frr.rs
+++ b/proxmox-ve-config/src/sdn/frr.rs
@@ -1,6 +1,4 @@
-use std::collections::{BTreeMap, BTreeSet};
-
-use proxmox_frr::FrrConfig;
+use proxmox_frr::{FrrConfig, OpenfabricFrrConfig, OspfFrrConfig};
 
 use crate::common::valid::Valid;
 use crate::sdn::fabric::{section_config::node::NodeId, FabricConfig};
@@ -28,11 +26,8 @@ impl FrrConfigBuilder {
     /// interface if there isn't a more specific one.)
     pub fn build(self, current_node: NodeId) -> Result<FrrConfig, anyhow::Error> {
         let mut frr_config = FrrConfig {
-            router: BTreeMap::new(),
-            interfaces: BTreeMap::new(),
-            access_lists: Vec::new(),
-            routemaps: Vec::new(),
-            protocol_routemaps: BTreeSet::new(),
+            openfabric: OpenfabricFrrConfig::default(),
+            ospf: OspfFrrConfig::default(),
         };
 
         crate::sdn::fabric::frr::build_fabric(current_node, self.fabrics, &mut frr_config)?;
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [pve-devel] [PATCH ve-rs 4/4] tests: always prepend the frr delimiter/comment "!" to the block
  2025-09-19  9:41 [pve-devel] [RFC network/ve-rs 0/8] Template-based FRR config generation Gabriel Goller
                   ` (2 preceding siblings ...)
  2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 3/4] ve-config: fabrics: use new proxmox-frr structs to generate frr config Gabriel Goller
@ 2025-09-19  9:41 ` Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH network 1/4] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Gabriel Goller @ 2025-09-19  9:41 UTC (permalink / raw)
  To: pve-devel

In pve-network we always prepend the "!" delimiter/comment to blocks in
the frr config. Change to prepend in the templates as well so that we
don't have any duplicate "!!" in the config where rust and perl
generated config meet.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 .../fabric/snapshots/fabric__openfabric_default_pve.snap  | 2 +-
 .../fabric/snapshots/fabric__openfabric_default_pve1.snap | 2 +-
 .../snapshots/fabric__openfabric_dualstack_pve.snap       | 8 ++++----
 .../snapshots/fabric__openfabric_ipv6_only_pve.snap       | 2 +-
 .../snapshots/fabric__openfabric_multi_fabric_pve1.snap   | 2 +-
 .../tests/fabric/snapshots/fabric__ospf_default_pve.snap  | 2 +-
 .../tests/fabric/snapshots/fabric__ospf_default_pve1.snap | 2 +-
 .../fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap  | 2 +-
 8 files changed, 11 insertions(+), 11 deletions(-)

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..89435b23bf53 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
@@ -3,25 +3,26 @@ 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
@@ -43,4 +44,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..89d788c9bc7e 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
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
 expression: output
 snapshot_kind: text
 ---
+!
 router openfabric uwu
  net 49.0001.0000.0000.000a.00
 exit
@@ -31,4 +32,3 @@ route-map pve_openfabric6 permit 100
 exit
 !
 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



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [pve-devel] [PATCH network 1/4] sdn: remove duplicate comment line '!' in frr config
  2025-09-19  9:41 [pve-devel] [RFC network/ve-rs 0/8] Template-based FRR config generation Gabriel Goller
                   ` (3 preceding siblings ...)
  2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 4/4] tests: always prepend the frr delimiter/comment "!" to the block Gabriel Goller
@ 2025-09-19  9:41 ` Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH network 2/4] sdn: add trailing newline " Gabriel Goller
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Gabriel Goller @ 2025-09-19  9:41 UTC (permalink / raw)
  To: pve-devel

The '!' symbol in frr is just a separator/comment and doesn't do
anything. We usually add the '!' and the start of a block, so we remove
it after the start block.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN/Frr.pm                                       | 1 -
 src/test/zones/evpn/advertise_subnets/expected_controller_config | 1 -
 .../evpn/disable_arp_nd_suppression/expected_controller_config   | 1 -
 src/test/zones/evpn/ebgp/expected_controller_config              | 1 -
 src/test/zones/evpn/ebgp_loopback/expected_controller_config     | 1 -
 src/test/zones/evpn/exitnode/expected_controller_config          | 1 -
 .../zones/evpn/exitnode_local_routing/expected_controller_config | 1 -
 src/test/zones/evpn/exitnode_primary/expected_controller_config  | 1 -
 src/test/zones/evpn/exitnode_snat/expected_controller_config     | 1 -
 src/test/zones/evpn/exitnodenullroute/expected_controller_config | 1 -
 src/test/zones/evpn/ipv4/expected_controller_config              | 1 -
 src/test/zones/evpn/ipv4ipv6/expected_controller_config          | 1 -
 src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config | 1 -
 src/test/zones/evpn/ipv6/expected_controller_config              | 1 -
 src/test/zones/evpn/ipv6underlay/expected_controller_config      | 1 -
 src/test/zones/evpn/isis/expected_controller_config              | 1 -
 src/test/zones/evpn/isis_loopback/expected_controller_config     | 1 -
 src/test/zones/evpn/isis_standalone/expected_controller_config   | 1 -
 src/test/zones/evpn/multipath_relax/expected_controller_config   | 1 -
 src/test/zones/evpn/multiplezones/expected_controller_config     | 1 -
 src/test/zones/evpn/openfabric_fabric/expected_controller_config | 1 -
 src/test/zones/evpn/ospf_fabric/expected_controller_config       | 1 -
 src/test/zones/evpn/rt_import/expected_controller_config         | 1 -
 src/test/zones/evpn/vxlanport/expected_controller_config         | 1 -
 24 files changed, 24 deletions(-)

diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
index b607b32c248d..32ea37d813fb 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -217,7 +217,6 @@ sub raw_config_to_string {
         "hostname $nodename",
         "log syslog informational",
         "service integrated-vtysh-config",
-        "!",
     );
 
     push @final_config, @$raw_config;
diff --git a/src/test/zones/evpn/advertise_subnets/expected_controller_config b/src/test/zones/evpn/advertise_subnets/expected_controller_config
index 6bf36f6ed331..40d08465b1e2 100644
--- a/src/test/zones/evpn/advertise_subnets/expected_controller_config
+++ b/src/test/zones/evpn/advertise_subnets/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config b/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config
index 8a133a506b52..4b5d628b65e2 100644
--- a/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config
+++ b/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/ebgp/expected_controller_config b/src/test/zones/evpn/ebgp/expected_controller_config
index 5cf695c97cab..1629a2deb454 100644
--- a/src/test/zones/evpn/ebgp/expected_controller_config
+++ b/src/test/zones/evpn/ebgp/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/ebgp_loopback/expected_controller_config b/src/test/zones/evpn/ebgp_loopback/expected_controller_config
index 4b44a0702d01..fc8901de6173 100644
--- a/src/test/zones/evpn/ebgp_loopback/expected_controller_config
+++ b/src/test/zones/evpn/ebgp_loopback/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/exitnode/expected_controller_config b/src/test/zones/evpn/exitnode/expected_controller_config
index 9310d51d61bb..02bb7609431b 100644
--- a/src/test/zones/evpn/exitnode/expected_controller_config
+++ b/src/test/zones/evpn/exitnode/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/exitnode_local_routing/expected_controller_config b/src/test/zones/evpn/exitnode_local_routing/expected_controller_config
index 6735d4d83698..96fb26d6a80f 100644
--- a/src/test/zones/evpn/exitnode_local_routing/expected_controller_config
+++ b/src/test/zones/evpn/exitnode_local_routing/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/exitnode_primary/expected_controller_config b/src/test/zones/evpn/exitnode_primary/expected_controller_config
index b2f3b332529b..7ea32db093cf 100644
--- a/src/test/zones/evpn/exitnode_primary/expected_controller_config
+++ b/src/test/zones/evpn/exitnode_primary/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/exitnode_snat/expected_controller_config b/src/test/zones/evpn/exitnode_snat/expected_controller_config
index 9310d51d61bb..02bb7609431b 100644
--- a/src/test/zones/evpn/exitnode_snat/expected_controller_config
+++ b/src/test/zones/evpn/exitnode_snat/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/exitnodenullroute/expected_controller_config b/src/test/zones/evpn/exitnodenullroute/expected_controller_config
index b99c1a1da71a..a36da27ca756 100644
--- a/src/test/zones/evpn/exitnodenullroute/expected_controller_config
+++ b/src/test/zones/evpn/exitnodenullroute/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
  ip route 10.0.0.0/24 null0
diff --git a/src/test/zones/evpn/ipv4/expected_controller_config b/src/test/zones/evpn/ipv4/expected_controller_config
index 8a133a506b52..4b5d628b65e2 100644
--- a/src/test/zones/evpn/ipv4/expected_controller_config
+++ b/src/test/zones/evpn/ipv4/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/ipv4ipv6/expected_controller_config b/src/test/zones/evpn/ipv4ipv6/expected_controller_config
index 8a133a506b52..4b5d628b65e2 100644
--- a/src/test/zones/evpn/ipv4ipv6/expected_controller_config
+++ b/src/test/zones/evpn/ipv4ipv6/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config b/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config
index 8a133a506b52..4b5d628b65e2 100644
--- a/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config
+++ b/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/ipv6/expected_controller_config b/src/test/zones/evpn/ipv6/expected_controller_config
index 8a133a506b52..4b5d628b65e2 100644
--- a/src/test/zones/evpn/ipv6/expected_controller_config
+++ b/src/test/zones/evpn/ipv6/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/ipv6underlay/expected_controller_config b/src/test/zones/evpn/ipv6underlay/expected_controller_config
index b07791434489..d7c693a2e213 100644
--- a/src/test/zones/evpn/ipv6underlay/expected_controller_config
+++ b/src/test/zones/evpn/ipv6underlay/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/isis/expected_controller_config b/src/test/zones/evpn/isis/expected_controller_config
index 068b08a09f77..efe2d43cd583 100644
--- a/src/test/zones/evpn/isis/expected_controller_config
+++ b/src/test/zones/evpn/isis/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/isis_loopback/expected_controller_config b/src/test/zones/evpn/isis_loopback/expected_controller_config
index 9b3dc449b120..b9d9c2969421 100644
--- a/src/test/zones/evpn/isis_loopback/expected_controller_config
+++ b/src/test/zones/evpn/isis_loopback/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/isis_standalone/expected_controller_config b/src/test/zones/evpn/isis_standalone/expected_controller_config
index ce62d74c0d60..bab5ec9119a2 100644
--- a/src/test/zones/evpn/isis_standalone/expected_controller_config
+++ b/src/test/zones/evpn/isis_standalone/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 interface eth0
  ip router isis isis1
 !
diff --git a/src/test/zones/evpn/multipath_relax/expected_controller_config b/src/test/zones/evpn/multipath_relax/expected_controller_config
index bf2289093b6f..7dff84cdd057 100644
--- a/src/test/zones/evpn/multipath_relax/expected_controller_config
+++ b/src/test/zones/evpn/multipath_relax/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/multiplezones/expected_controller_config b/src/test/zones/evpn/multiplezones/expected_controller_config
index e1e6df5562e0..849d36a07e35 100644
--- a/src/test/zones/evpn/multiplezones/expected_controller_config
+++ b/src/test/zones/evpn/multiplezones/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/openfabric_fabric/expected_controller_config b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
index 2c29f3cc4a5f..cff540311c8b 100644
--- a/src/test/zones/evpn/openfabric_fabric/expected_controller_config
+++ b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_evpn
  vni 100
 exit-vrf
diff --git a/src/test/zones/evpn/ospf_fabric/expected_controller_config b/src/test/zones/evpn/ospf_fabric/expected_controller_config
index 712fc2a9fd4f..fd3651e1275c 100644
--- a/src/test/zones/evpn/ospf_fabric/expected_controller_config
+++ b/src/test/zones/evpn/ospf_fabric/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_evpn
  vni 100
 exit-vrf
diff --git a/src/test/zones/evpn/rt_import/expected_controller_config b/src/test/zones/evpn/rt_import/expected_controller_config
index 4de7f89f82c8..a4c0c6605d70 100644
--- a/src/test/zones/evpn/rt_import/expected_controller_config
+++ b/src/test/zones/evpn/rt_import/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
diff --git a/src/test/zones/evpn/vxlanport/expected_controller_config b/src/test/zones/evpn/vxlanport/expected_controller_config
index 8a133a506b52..4b5d628b65e2 100644
--- a/src/test/zones/evpn/vxlanport/expected_controller_config
+++ b/src/test/zones/evpn/vxlanport/expected_controller_config
@@ -4,7 +4,6 @@ hostname localhost
 log syslog informational
 service integrated-vtysh-config
 !
-!
 vrf vrf_myzone
  vni 1000
 exit-vrf
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [pve-devel] [PATCH network 2/4] sdn: add trailing newline in frr config
  2025-09-19  9:41 [pve-devel] [RFC network/ve-rs 0/8] Template-based FRR config generation Gabriel Goller
                   ` (4 preceding siblings ...)
  2025-09-19  9:41 ` [pve-devel] [PATCH network 1/4] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
@ 2025-09-19  9:41 ` Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH network 3/4] sdn: tests: add missing comment '!' " Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH network 4/4] tests: use Test::Differences to make test assertions Gabriel Goller
  7 siblings, 0 replies; 9+ messages in thread
From: Gabriel Goller @ 2025-09-19  9:41 UTC (permalink / raw)
  To: pve-devel

Add trailing newline to the end of frr config, it looks better this way.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN/Frr.pm                                      | 2 +-
 .../zones/evpn/advertise_subnets/expected_controller_config     | 2 +-
 .../evpn/disable_arp_nd_suppression/expected_controller_config  | 2 +-
 src/test/zones/evpn/ebgp/expected_controller_config             | 2 +-
 src/test/zones/evpn/ebgp_loopback/expected_controller_config    | 2 +-
 src/test/zones/evpn/exitnode/expected_controller_config         | 2 +-
 .../evpn/exitnode_local_routing/expected_controller_config      | 2 +-
 src/test/zones/evpn/exitnode_primary/expected_controller_config | 2 +-
 src/test/zones/evpn/exitnode_snat/expected_controller_config    | 2 +-
 .../zones/evpn/exitnodenullroute/expected_controller_config     | 2 +-
 src/test/zones/evpn/ipv4/expected_controller_config             | 2 +-
 src/test/zones/evpn/ipv4ipv6/expected_controller_config         | 2 +-
 .../zones/evpn/ipv4ipv6nogateway/expected_controller_config     | 2 +-
 src/test/zones/evpn/ipv6/expected_controller_config             | 2 +-
 src/test/zones/evpn/ipv6underlay/expected_controller_config     | 2 +-
 src/test/zones/evpn/isis/expected_controller_config             | 2 +-
 src/test/zones/evpn/isis_loopback/expected_controller_config    | 2 +-
 src/test/zones/evpn/isis_standalone/expected_controller_config  | 2 +-
 src/test/zones/evpn/multipath_relax/expected_controller_config  | 2 +-
 src/test/zones/evpn/multiplezones/expected_controller_config    | 2 +-
 .../zones/evpn/openfabric_fabric/expected_controller_config     | 2 +-
 src/test/zones/evpn/ospf_fabric/expected_controller_config      | 2 +-
 src/test/zones/evpn/rt_import/expected_controller_config        | 2 +-
 src/test/zones/evpn/vxlanport/expected_controller_config        | 2 +-
 24 files changed, 24 insertions(+), 24 deletions(-)

diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
index 32ea37d813fb..ff46b6631328 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -222,7 +222,7 @@ sub raw_config_to_string {
     push @final_config, @$raw_config;
 
     push @final_config, (
-        "!", "line vty", "!",
+        "!", "line vty", "!", "",
     );
 
     return join("\n", @final_config);
diff --git a/src/test/zones/evpn/advertise_subnets/expected_controller_config b/src/test/zones/evpn/advertise_subnets/expected_controller_config
index 40d08465b1e2..08e47e86a2ec 100644
--- a/src/test/zones/evpn/advertise_subnets/expected_controller_config
+++ b/src/test/zones/evpn/advertise_subnets/expected_controller_config
@@ -54,4 +54,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config b/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config
index 4b5d628b65e2..5458aa17b854 100644
--- a/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config
+++ b/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config
@@ -41,4 +41,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/ebgp/expected_controller_config b/src/test/zones/evpn/ebgp/expected_controller_config
index 1629a2deb454..410e8af3e7b8 100644
--- a/src/test/zones/evpn/ebgp/expected_controller_config
+++ b/src/test/zones/evpn/ebgp/expected_controller_config
@@ -58,4 +58,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/ebgp_loopback/expected_controller_config b/src/test/zones/evpn/ebgp_loopback/expected_controller_config
index fc8901de6173..2358937ffbf6 100644
--- a/src/test/zones/evpn/ebgp_loopback/expected_controller_config
+++ b/src/test/zones/evpn/ebgp_loopback/expected_controller_config
@@ -70,4 +70,4 @@ exit
 ip protocol bgp route-map correct_src
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/exitnode/expected_controller_config b/src/test/zones/evpn/exitnode/expected_controller_config
index 02bb7609431b..631f3acd7b62 100644
--- a/src/test/zones/evpn/exitnode/expected_controller_config
+++ b/src/test/zones/evpn/exitnode/expected_controller_config
@@ -74,4 +74,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/exitnode_local_routing/expected_controller_config b/src/test/zones/evpn/exitnode_local_routing/expected_controller_config
index 96fb26d6a80f..3da3a79a44b3 100644
--- a/src/test/zones/evpn/exitnode_local_routing/expected_controller_config
+++ b/src/test/zones/evpn/exitnode_local_routing/expected_controller_config
@@ -60,4 +60,4 @@ exit
 ip route 10.0.0.0/24 10.255.255.2 xvrf_myzone
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/exitnode_primary/expected_controller_config b/src/test/zones/evpn/exitnode_primary/expected_controller_config
index 7ea32db093cf..d3d3da37803b 100644
--- a/src/test/zones/evpn/exitnode_primary/expected_controller_config
+++ b/src/test/zones/evpn/exitnode_primary/expected_controller_config
@@ -76,4 +76,4 @@ route-map MAP_VTEP_OUT permit 3
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/exitnode_snat/expected_controller_config b/src/test/zones/evpn/exitnode_snat/expected_controller_config
index 02bb7609431b..631f3acd7b62 100644
--- a/src/test/zones/evpn/exitnode_snat/expected_controller_config
+++ b/src/test/zones/evpn/exitnode_snat/expected_controller_config
@@ -74,4 +74,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/exitnodenullroute/expected_controller_config b/src/test/zones/evpn/exitnodenullroute/expected_controller_config
index a36da27ca756..6b9fc01e67c8 100644
--- a/src/test/zones/evpn/exitnodenullroute/expected_controller_config
+++ b/src/test/zones/evpn/exitnodenullroute/expected_controller_config
@@ -117,4 +117,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/ipv4/expected_controller_config b/src/test/zones/evpn/ipv4/expected_controller_config
index 4b5d628b65e2..5458aa17b854 100644
--- a/src/test/zones/evpn/ipv4/expected_controller_config
+++ b/src/test/zones/evpn/ipv4/expected_controller_config
@@ -41,4 +41,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/ipv4ipv6/expected_controller_config b/src/test/zones/evpn/ipv4ipv6/expected_controller_config
index 4b5d628b65e2..5458aa17b854 100644
--- a/src/test/zones/evpn/ipv4ipv6/expected_controller_config
+++ b/src/test/zones/evpn/ipv4ipv6/expected_controller_config
@@ -41,4 +41,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config b/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config
index 4b5d628b65e2..5458aa17b854 100644
--- a/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config
+++ b/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config
@@ -41,4 +41,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/ipv6/expected_controller_config b/src/test/zones/evpn/ipv6/expected_controller_config
index 4b5d628b65e2..5458aa17b854 100644
--- a/src/test/zones/evpn/ipv6/expected_controller_config
+++ b/src/test/zones/evpn/ipv6/expected_controller_config
@@ -41,4 +41,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/ipv6underlay/expected_controller_config b/src/test/zones/evpn/ipv6underlay/expected_controller_config
index d7c693a2e213..7b9d73784482 100644
--- a/src/test/zones/evpn/ipv6underlay/expected_controller_config
+++ b/src/test/zones/evpn/ipv6underlay/expected_controller_config
@@ -41,4 +41,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/isis/expected_controller_config b/src/test/zones/evpn/isis/expected_controller_config
index efe2d43cd583..77499476570e 100644
--- a/src/test/zones/evpn/isis/expected_controller_config
+++ b/src/test/zones/evpn/isis/expected_controller_config
@@ -54,4 +54,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/isis_loopback/expected_controller_config b/src/test/zones/evpn/isis_loopback/expected_controller_config
index b9d9c2969421..4cdfb9354c8d 100644
--- a/src/test/zones/evpn/isis_loopback/expected_controller_config
+++ b/src/test/zones/evpn/isis_loopback/expected_controller_config
@@ -55,4 +55,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/isis_standalone/expected_controller_config b/src/test/zones/evpn/isis_standalone/expected_controller_config
index bab5ec9119a2..0739cb5e1b3a 100644
--- a/src/test/zones/evpn/isis_standalone/expected_controller_config
+++ b/src/test/zones/evpn/isis_standalone/expected_controller_config
@@ -18,4 +18,4 @@ router isis isis1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/multipath_relax/expected_controller_config b/src/test/zones/evpn/multipath_relax/expected_controller_config
index 7dff84cdd057..a72c48167161 100644
--- a/src/test/zones/evpn/multipath_relax/expected_controller_config
+++ b/src/test/zones/evpn/multipath_relax/expected_controller_config
@@ -53,4 +53,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/multiplezones/expected_controller_config b/src/test/zones/evpn/multiplezones/expected_controller_config
index 849d36a07e35..f7df034c5c34 100644
--- a/src/test/zones/evpn/multiplezones/expected_controller_config
+++ b/src/test/zones/evpn/multiplezones/expected_controller_config
@@ -51,4 +51,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/openfabric_fabric/expected_controller_config b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
index cff540311c8b..ad13359b23d4 100644
--- a/src/test/zones/evpn/openfabric_fabric/expected_controller_config
+++ b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
@@ -70,4 +70,4 @@ ip protocol openfabric route-map pve_openfabric
 !
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/ospf_fabric/expected_controller_config b/src/test/zones/evpn/ospf_fabric/expected_controller_config
index fd3651e1275c..c124c3aa33c7 100644
--- a/src/test/zones/evpn/ospf_fabric/expected_controller_config
+++ b/src/test/zones/evpn/ospf_fabric/expected_controller_config
@@ -64,4 +64,4 @@ ip protocol ospf route-map pve_ospf
 !
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/rt_import/expected_controller_config b/src/test/zones/evpn/rt_import/expected_controller_config
index a4c0c6605d70..8dc74560e66c 100644
--- a/src/test/zones/evpn/rt_import/expected_controller_config
+++ b/src/test/zones/evpn/rt_import/expected_controller_config
@@ -47,4 +47,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
diff --git a/src/test/zones/evpn/vxlanport/expected_controller_config b/src/test/zones/evpn/vxlanport/expected_controller_config
index 4b5d628b65e2..5458aa17b854 100644
--- a/src/test/zones/evpn/vxlanport/expected_controller_config
+++ b/src/test/zones/evpn/vxlanport/expected_controller_config
@@ -41,4 +41,4 @@ route-map MAP_VTEP_OUT permit 1
 exit
 !
 line vty
-!
\ No newline at end of file
+!
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [pve-devel] [PATCH network 3/4] sdn: tests: add missing comment '!' in frr config
  2025-09-19  9:41 [pve-devel] [RFC network/ve-rs 0/8] Template-based FRR config generation Gabriel Goller
                   ` (5 preceding siblings ...)
  2025-09-19  9:41 ` [pve-devel] [PATCH network 2/4] sdn: add trailing newline " Gabriel Goller
@ 2025-09-19  9:41 ` Gabriel Goller
  2025-09-19  9:41 ` [pve-devel] [PATCH network 4/4] tests: use Test::Differences to make test assertions Gabriel Goller
  7 siblings, 0 replies; 9+ messages in thread
From: Gabriel Goller @ 2025-09-19  9:41 UTC (permalink / raw)
  To: pve-devel

Previous patch in proxmox-frr fixed a few serializing issues, it also
adds a missing '!' (frr comment) inbetween the fabrics config and the
controller config.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 .../zones/evpn/openfabric_fabric/expected_controller_config     | 2 +-
 src/test/zones/evpn/ospf_fabric/expected_controller_config      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/test/zones/evpn/openfabric_fabric/expected_controller_config b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
index ad13359b23d4..742566b5933b 100644
--- a/src/test/zones/evpn/openfabric_fabric/expected_controller_config
+++ b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
@@ -40,6 +40,7 @@ exit
 !
 route-map MAP_VTEP_OUT permit 1
 exit
+!
 router openfabric test
  net 49.0001.1720.2000.3001.00
 exit
@@ -68,6 +69,5 @@ exit
 !
 ip protocol openfabric route-map pve_openfabric
 !
-!
 line vty
 !
diff --git a/src/test/zones/evpn/ospf_fabric/expected_controller_config b/src/test/zones/evpn/ospf_fabric/expected_controller_config
index c124c3aa33c7..4424927235c1 100644
--- a/src/test/zones/evpn/ospf_fabric/expected_controller_config
+++ b/src/test/zones/evpn/ospf_fabric/expected_controller_config
@@ -40,6 +40,7 @@ exit
 !
 route-map MAP_VTEP_OUT permit 1
 exit
+!
 router ospf
  ospf router-id 172.20.30.1
 exit
@@ -62,6 +63,5 @@ exit
 !
 ip protocol ospf route-map pve_ospf
 !
-!
 line vty
 !
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [pve-devel] [PATCH network 4/4] tests: use Test::Differences to make test assertions
  2025-09-19  9:41 [pve-devel] [RFC network/ve-rs 0/8] Template-based FRR config generation Gabriel Goller
                   ` (6 preceding siblings ...)
  2025-09-19  9:41 ` [pve-devel] [PATCH network 3/4] sdn: tests: add missing comment '!' " Gabriel Goller
@ 2025-09-19  9:41 ` Gabriel Goller
  7 siblings, 0 replies; 9+ messages in thread
From: Gabriel Goller @ 2025-09-19  9:41 UTC (permalink / raw)
  To: pve-devel

Test::Differences provides a much better output. Instead of dumping the
whole file, eq_or_diff shows a pretty diff table with the changes
side-by-side. It also shows a small context and not the whole
expected/got file. This is very useful when especially when tests fails
on bigger config files.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 debian/control                      |  1 +
 src/test/run_test_dns.pl            | 15 +++++++-------
 src/test/run_test_ipams.pl          | 13 ++++++------
 src/test/run_test_subnets.pl        | 31 +++++++++++++++--------------
 src/test/run_test_vnets_blackbox.pl | 17 ++++++++--------
 src/test/run_test_zones.pl          |  5 +++--
 6 files changed, 44 insertions(+), 38 deletions(-)

diff --git a/debian/control b/debian/control
index b45d12621aab..0501391ef282 100644
--- a/debian/control
+++ b/debian/control
@@ -8,6 +8,7 @@ Build-Depends: debhelper-compat (= 13),
                libnet-subnet-perl,
                libpve-rs-perl (>= 0.10.3),
                libtest-mockmodule-perl,
+               libtest-differences-perl,
                perl,
                pve-cluster (>= 9.0.1),
                pve-firewall (>= 5.1.0~),
diff --git a/src/test/run_test_dns.pl b/src/test/run_test_dns.pl
index 26fbfaf035d2..f099da04a777 100755
--- a/src/test/run_test_dns.pl
+++ b/src/test/run_test_dns.pl
@@ -9,6 +9,7 @@ use Net::IP;
 
 use Test::More;
 use Test::MockModule;
+use Test::Differences;
 
 use PVE::Network::SDN;
 use PVE::Network::SDN::Zones;
@@ -101,7 +102,7 @@ foreach my $path (@plugins) {
         $plugin->add_a_record($plugin_config, $zone, $hostname, $ip, 1);
 
         if ($@) {
-            is($@, $expected, $name);
+            eq_or_diff($@, $expected, $name);
         } else {
             fail($name);
         }
@@ -114,7 +115,7 @@ foreach my $path (@plugins) {
         $plugin->add_ptr_record($plugin_config, $zone, $hostname, $ip, 1);
 
         if ($@) {
-            is($@, $expected, $name);
+            eq_or_diff($@, $expected, $name);
         } else {
             fail($name);
         }
@@ -127,7 +128,7 @@ foreach my $path (@plugins) {
         $plugin->del_ptr_record($plugin_config, $zone, $ip, 1);
 
         if ($@) {
-            is($@, $expected, $name);
+            eq_or_diff($@, $expected, $name);
         } else {
             fail($name);
         }
@@ -167,7 +168,7 @@ foreach my $path (@plugins) {
         $plugin->del_a_record($plugin_config, $zone, $hostname, $ip, 1);
 
         if ($@) {
-            is($@, $expected, $name);
+            eq_or_diff($@, $expected, $name);
         } else {
             fail($name);
         }
@@ -212,7 +213,7 @@ foreach my $path (@plugins) {
         $plugin->del_a_record($plugin_config, $zone, $hostname, $ip, 1);
 
         if ($@) {
-            is($@, $expected, $name);
+            eq_or_diff($@, $expected, $name);
         } else {
             fail($name);
         }
@@ -250,7 +251,7 @@ foreach my $path (@plugins) {
         $plugin->add_a_record($plugin_config, $zone, $hostname, $ip, 1);
 
         if ($@) {
-            is($@, $expected, $name);
+            eq_or_diff($@, $expected, $name);
         } else {
             fail($name);
         }
@@ -264,7 +265,7 @@ foreach my $path (@plugins) {
     $plugin->verify_zone($plugin_config, $zone, 1);
 
     if ($@) {
-        is($@, $expected, $name);
+        eq_or_diff($@, $expected, $name);
     } else {
         fail($name);
     }
diff --git a/src/test/run_test_ipams.pl b/src/test/run_test_ipams.pl
index 193b34bb98d9..f9c44fe1a3b9 100755
--- a/src/test/run_test_ipams.pl
+++ b/src/test/run_test_ipams.pl
@@ -8,6 +8,7 @@ use File::Slurp;
 
 use Test::More;
 use Test::MockModule;
+use Test::Differences;
 
 use PVE::Network::SDN;
 use PVE::Network::SDN::Zones;
@@ -120,7 +121,7 @@ foreach my $path (@plugins) {
     );
 
     if ($@) {
-        is($@, $expected, $name);
+        eq_or_diff($@, $expected, $name);
     } else {
         fail($name);
     }
@@ -133,7 +134,7 @@ foreach my $path (@plugins) {
     $plugin->add_next_freeip($plugin_config, $subnetid, $subnet, $hostname, $mac, $description, 1);
 
     if ($@) {
-        is($@, $expected, $name);
+        eq_or_diff($@, $expected, $name);
     } else {
         fail($name);
     }
@@ -146,7 +147,7 @@ foreach my $path (@plugins) {
     $plugin->del_ip($plugin_config, $subnetid, $subnet, $ip, 1);
 
     if ($@) {
-        is($@, $expected, $name);
+        eq_or_diff($@, $expected, $name);
     } else {
         fail($name);
     }
@@ -168,7 +169,7 @@ foreach my $path (@plugins) {
     );
 
     if ($@) {
-        is($@, $expected, $name);
+        eq_or_diff($@, $expected, $name);
     } else {
         fail($name);
     }
@@ -192,7 +193,7 @@ foreach my $path (@plugins) {
     );
 
     if ($@) {
-        is($@, $expected, $name);
+        eq_or_diff($@, $expected, $name);
     } else {
         fail($name);
     }
@@ -211,7 +212,7 @@ foreach my $path (@plugins) {
     $plugin->add_subnet($plugin_config, $subnetid, $subnet, 1);
 
     if ($@) {
-        is($@, $expected, $name);
+        eq_or_diff($@, $expected, $name);
     } else {
         fail($name);
     }
diff --git a/src/test/run_test_subnets.pl b/src/test/run_test_subnets.pl
index d4f8d6616de3..16443bf7ee26 100755
--- a/src/test/run_test_subnets.pl
+++ b/src/test/run_test_subnets.pl
@@ -8,6 +8,7 @@ use File::Slurp;
 
 use Test::More;
 use Test::MockModule;
+use Test::Differences;
 
 use PVE::Network::SDN;
 use PVE::Network::SDN::Zones;
@@ -147,9 +148,9 @@ foreach my $path (@plugins) {
         fail("$name : $@");
     } elsif ($ipam) {
         $result = $js->encode($plugin->read_db());
-        is($result, $expected, $name);
+        eq_or_diff($result, $expected, $name);
     } else {
-        is(undef, undef, $name);
+        eq_or_diff(undef, undef, $name);
     }
 
     ## add_ip
@@ -173,9 +174,9 @@ foreach my $path (@plugins) {
         fail("$name : $@");
     } elsif ($ipam) {
         $result = $js->encode($plugin->read_db());
-        is($result, $expected, $name);
+        eq_or_diff($result, $expected, $name);
     } else {
-        is(undef, undef, $name);
+        eq_or_diff(undef, undef, $name);
     }
 
     if ($ipam) {
@@ -190,7 +191,7 @@ foreach my $path (@plugins) {
         };
 
         if ($@) {
-            is(undef, undef, $name);
+            eq_or_diff(undef, undef, $name);
         } else {
             fail("$name : $@");
         }
@@ -225,9 +226,9 @@ foreach my $path (@plugins) {
         fail("$name : $@");
     } elsif ($ipam) {
         $result = $js->encode($plugin->read_db());
-        is($result, $expected, $name);
+        eq_or_diff($result, $expected, $name);
     } else {
-        is(undef, undef, $name);
+        eq_or_diff(undef, undef, $name);
     }
 
     ## add_next_free
@@ -266,7 +267,7 @@ foreach my $path (@plugins) {
         fail("$name : $@");
     } elsif ($ipam) {
         $result = $js->encode($plugin->read_db());
-        is($result, $expected, $name);
+        eq_or_diff($result, $expected, $name);
     }
 
     ## del_ip
@@ -299,9 +300,9 @@ foreach my $path (@plugins) {
         fail("$name : $@");
     } elsif ($ipam) {
         $result = $js->encode($plugin->read_db());
-        is($result, $expected, $name);
+        eq_or_diff($result, $expected, $name);
     } else {
-        is(undef, undef, $name);
+        eq_or_diff(undef, undef, $name);
     }
 
     if ($ipam) {
@@ -314,7 +315,7 @@ foreach my $path (@plugins) {
         eval { PVE::Network::SDN::Subnets::del_subnet($zone, $subnetid, $subnet); };
 
         if ($@) {
-            is($result, $expected, $name);
+            eq_or_diff($result, $expected, $name);
         } else {
             fail("$name : $@");
         }
@@ -367,9 +368,9 @@ foreach my $path (@plugins) {
     if ($@) {
         if ($ipam) {
             $result = $js->encode($plugin->read_db());
-            is($result, $expected, $name);
+            eq_or_diff($result, $expected, $name);
         } else {
-            is(undef, undef, $name);
+            eq_or_diff(undef, undef, $name);
         }
     } else {
         fail("$name : $@");
@@ -390,9 +391,9 @@ foreach my $path (@plugins) {
         fail("$name : $@");
     } elsif ($ipam) {
         $result = $js->encode($plugin->read_db());
-        is($result, $expected, $name);
+        eq_or_diff($result, $expected, $name);
     } else {
-        is(undef, undef, $name);
+        eq_or_diff(undef, undef, $name);
     }
 
 }
diff --git a/src/test/run_test_vnets_blackbox.pl b/src/test/run_test_vnets_blackbox.pl
index 468b1ede17e2..a7025dc36e75 100755
--- a/src/test/run_test_vnets_blackbox.pl
+++ b/src/test/run_test_vnets_blackbox.pl
@@ -10,6 +10,7 @@ use NetAddr::IP qw(:lower);
 
 use Test::More;
 use Test::MockModule;
+use Test::Differences;
 
 use PVE::Tools qw(extract_param file_set_contents);
 
@@ -416,7 +417,7 @@ sub test_without_subnet {
 
     my @ips = get_ips_from_mac($mac);
     my $num_ips = scalar @ips;
-    is($num_ips, 0, "$test_name: No IP allocated in IPAM");
+    eq_or_diff($num_ips, 0, "$test_name: No IP allocated in IPAM");
 }
 run_test(\&test_without_subnet);
 
@@ -463,7 +464,7 @@ sub test_nic_join {
 
     my @ips = get_ips_from_mac($mac);
     my $num_ips = scalar @ips;
-    is($num_ips, $num_subnets, "$test_name: Expecting $num_subnets IPs, found $num_ips");
+    eq_or_diff($num_ips, $num_subnets, "$test_name: Expecting $num_subnets IPs, found $num_ips");
     ok(
         (all { ($_->{vnet} eq $vnetid && $_->{zone} eq $zoneid) } @ips),
         "$test_name: all IPs in correct vnet and zone",
@@ -622,7 +623,7 @@ sub test_nic_join_full_dhcp_range {
 
     my @ips = get_ips_from_mac($mac);
     my $num_ips = scalar @ips;
-    is($num_ips, 0, "$test_name: No IP allocated in IPAM");
+    eq_or_diff($num_ips, 0, "$test_name: No IP allocated in IPAM");
 }
 
 run_test(
@@ -770,9 +771,9 @@ sub test_nic_start {
         });
     }
     my @current_ips = get_ips_from_mac($mac);
-    is(get_ip4(@current_ips), $current_ip4, "$test_name: setup current IPv4: $current_ip4")
+    eq_or_diff(get_ip4(@current_ips), $current_ip4, "$test_name: setup current IPv4: $current_ip4")
         if defined $current_ip4;
-    is(get_ip6(@current_ips), $current_ip6, "$test_name: setup current IPv6: $current_ip6")
+    eq_or_diff(get_ip6(@current_ips), $current_ip6, "$test_name: setup current IPv6: $current_ip6")
         if defined $current_ip6;
 
     eval { nic_start($vnetid, $mac, $hostname, $vmid); };
@@ -784,14 +785,14 @@ sub test_nic_start {
 
     my @ips = get_ips_from_mac($mac);
     my $num_ips = scalar @ips;
-    is($num_ips, $num_expected_ips, "$test_name: Expecting $num_expected_ips IPs, found $num_ips");
+    eq_or_diff($num_ips, $num_expected_ips, "$test_name: Expecting $num_expected_ips IPs, found $num_ips");
     ok(
         (all { ($_->{vnet} eq $vnetid && $_->{zone} eq $zoneid) } @ips),
         "$test_name: all IPs in correct vnet and zone",
     );
 
-    is(get_ip4(@ips), $current_ip4, "$test_name: still current IPv4: $current_ip4") if $current_ip4;
-    is(get_ip6(@ips), $current_ip6, "$test_name: still current IPv6: $current_ip6") if $current_ip6;
+    eq_or_diff(get_ip4(@ips), $current_ip4, "$test_name: still current IPv4: $current_ip4") if $current_ip4;
+    eq_or_diff(get_ip6(@ips), $current_ip6, "$test_name: still current IPv6: $current_ip6") if $current_ip6;
 }
 
 run_test(
diff --git a/src/test/run_test_zones.pl b/src/test/run_test_zones.pl
index 917f40a90069..905b2f42e1dc 100755
--- a/src/test/run_test_zones.pl
+++ b/src/test/run_test_zones.pl
@@ -8,6 +8,7 @@ use File::Slurp;
 
 use Test::More;
 use Test::MockModule;
+use Test::Differences;
 
 use PVE::Network::SDN;
 use PVE::Network::SDN::Zones;
@@ -140,7 +141,7 @@ foreach my $test (@tests) {
         diag("got unexpected error - $err");
         fail($name);
     } else {
-        is($result, $expected, $name);
+        eq_or_diff($result, $expected, $name);
     }
 
     if ($sdn_config->{controllers}) {
@@ -155,7 +156,7 @@ foreach my $test (@tests) {
             diag("got unexpected error - $err");
             fail($name);
         } else {
-            is($config, $expected, $name);
+            eq_or_diff($config, $expected, $name);
         }
     }
 }
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2025-09-19  9:42 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-09-19  9:41 [pve-devel] [RFC network/ve-rs 0/8] Template-based FRR config generation Gabriel Goller
2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 1/4] frr: add templates and structs to represent the frr config Gabriel Goller
2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 2/4] sdn-types: forward serialize to display for NET Gabriel Goller
2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 3/4] ve-config: fabrics: use new proxmox-frr structs to generate frr config Gabriel Goller
2025-09-19  9:41 ` [pve-devel] [PATCH ve-rs 4/4] tests: always prepend the frr delimiter/comment "!" to the block Gabriel Goller
2025-09-19  9:41 ` [pve-devel] [PATCH network 1/4] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
2025-09-19  9:41 ` [pve-devel] [PATCH network 2/4] sdn: add trailing newline " Gabriel Goller
2025-09-19  9:41 ` [pve-devel] [PATCH network 3/4] sdn: tests: add missing comment '!' " Gabriel Goller
2025-09-19  9:41 ` [pve-devel] [PATCH network 4/4] tests: use Test::Differences to make test assertions Gabriel Goller

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