* [PATCH proxmox-ve-rs v2 1/8] ve-config: firewall: cargo fmt
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 2/8] frr: add proxmox-frr-templates package that contains templates Gabriel Goller
` (17 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-ve-config/src/firewall/cluster.rs | 3 ++-
proxmox-ve-config/src/firewall/types/ipset.rs | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/proxmox-ve-config/src/firewall/cluster.rs b/proxmox-ve-config/src/firewall/cluster.rs
index 69d3bcd6d9cd..d379fe611772 100644
--- a/proxmox-ve-config/src/firewall/cluster.rs
+++ b/proxmox-ve-config/src/firewall/cluster.rs
@@ -147,7 +147,8 @@ mod tests {
log::{LogLevel, LogRateLimitTimescale},
rule::{Kind, RuleGroup},
rule_match::{
- Icmpv6, Icmpv6Code, Icmpv6Type, IpAddrMatch, IpMatch, Ports, Protocol, RuleMatch, Tcp, Udp
+ Icmpv6, Icmpv6Code, Icmpv6Type, IpAddrMatch, IpMatch, Ports, Protocol, RuleMatch, Tcp,
+ Udp,
},
};
diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs
index 3f93bb28423a..f09b8d82d6aa 100644
--- a/proxmox-ve-config/src/firewall/types/ipset.rs
+++ b/proxmox-ve-config/src/firewall/types/ipset.rs
@@ -313,7 +313,7 @@ impl Ipset {
Ok(())
}
- pub fn ipfilter(&self) -> Option<Ipfilter> {
+ pub fn ipfilter(&self) -> Option<Ipfilter<'_>> {
if self.name.scope() != IpsetScope::Guest {
return None;
}
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH proxmox-ve-rs v2 2/8] frr: add proxmox-frr-templates package that contains templates
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 1/8] ve-config: firewall: cargo fmt Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 3/8] ve-config: remove FrrConfigBuilder struct Gabriel Goller
` (16 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
This debian package contains the jinja template files used to generate
the frr config. Currently only the fabrics template files are here, the
rest will be added in later commits. When installing this package the
templates will be installed to `/usr/share/proxmox-frr/templates/`.
`proxmox-frr` will then use `include_str!` to embed the templates into
the binary.
This package will only be published to the `devel` channel.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
Makefile | 8 +++
proxmox-frr-templates/.gitignore | 1 +
proxmox-frr-templates/Makefile | 50 +++++++++++++++++++
proxmox-frr-templates/debian/changelog | 5 ++
proxmox-frr-templates/debian/control | 17 +++++++
proxmox-frr-templates/debian/copyright | 18 +++++++
.../debian/proxmox-frr-templates.install | 1 +
proxmox-frr-templates/debian/rules | 5 ++
.../templates/access_list.jinja | 6 +++
.../templates/access_lists.jinja | 6 +++
proxmox-frr-templates/templates/fabricd.jinja | 29 +++++++++++
.../templates/frr.conf.jinja | 5 ++
.../templates/interface.jinja | 9 ++++
proxmox-frr-templates/templates/ospfd.jinja | 18 +++++++
.../templates/protocol_routemaps.jinja | 10 ++++
.../templates/route_maps.jinja | 20 ++++++++
16 files changed, 208 insertions(+)
create mode 100644 proxmox-frr-templates/.gitignore
create mode 100644 proxmox-frr-templates/Makefile
create mode 100644 proxmox-frr-templates/debian/changelog
create mode 100644 proxmox-frr-templates/debian/control
create mode 100644 proxmox-frr-templates/debian/copyright
create mode 100644 proxmox-frr-templates/debian/proxmox-frr-templates.install
create mode 100755 proxmox-frr-templates/debian/rules
create mode 100644 proxmox-frr-templates/templates/access_list.jinja
create mode 100644 proxmox-frr-templates/templates/access_lists.jinja
create mode 100644 proxmox-frr-templates/templates/fabricd.jinja
create mode 100644 proxmox-frr-templates/templates/frr.conf.jinja
create mode 100644 proxmox-frr-templates/templates/interface.jinja
create mode 100644 proxmox-frr-templates/templates/ospfd.jinja
create mode 100644 proxmox-frr-templates/templates/protocol_routemaps.jinja
create mode 100644 proxmox-frr-templates/templates/route_maps.jinja
diff --git a/Makefile b/Makefile
index 812bc8e691bf..58e4475161a1 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,8 @@
CRATES != echo proxmox-*/Cargo.toml | sed -e 's|/Cargo.toml||g'
+BUILDDIR = build
+
# By default we just run checks:
.PHONY: all
all: check
@@ -25,6 +27,12 @@ dinstall:
$(MAKE) deb
sudo -k dpkg -i build/librust-*.deb
+proxmox-frr-templates-deb:
+ mkdir -p "${BUILDDIR}/proxmox-frr-templates"
+ cp -r proxmox-frr-templates/* "${BUILDDIR}/proxmox-frr-templates"
+ cd "${BUILDDIR}/proxmox-frr-templates"; dpkg-buildpackage -b -uc -us
+ touch $@
+
%-deb:
./build.sh $*
touch $@
diff --git a/proxmox-frr-templates/.gitignore b/proxmox-frr-templates/.gitignore
new file mode 100644
index 000000000000..796b96d1c402
--- /dev/null
+++ b/proxmox-frr-templates/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/proxmox-frr-templates/Makefile b/proxmox-frr-templates/Makefile
new file mode 100644
index 000000000000..0b010ed71e7a
--- /dev/null
+++ b/proxmox-frr-templates/Makefile
@@ -0,0 +1,50 @@
+include /usr/share/dpkg/default.mk
+
+PACKAGE=proxmox-frr-templates
+
+GITVERSION:=$(shell git rev-parse HEAD)
+
+BUILDDIR ?= build/$(PACKAGE)-$(DEB_VERSION)
+DSC=$(PACKAGE)_$(DEB_VERSION).dsc
+
+PVE_DEB=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION)_all.deb
+
+DEBS=$(PVE_DEB)
+
+all: deb
+deb: $(DEBS)
+
+$(BUILDDIR):
+ rm -rf $(BUILDDIR)
+ mkdir -p $(BUILDDIR)
+ cp -a debian $(BUILDDIR)/
+ cp -a templates $(BUILDDIR)/ || true
+ echo "git clone git://git.proxmox.com/git/proxmox-ve-rs.git\\ngit checkout $(GITVERSION)" > $(BUILDDIR)/debian/SOURCE
+
+$(PVE_DEB): $(BUILDDIR)
+ cd $(BUILDDIR); dpkg-buildpackage -b -uc -us
+ lintian build/$(DEBS)
+
+dsc: $(DSC)
+ $(MAKE) clean
+ $(MAKE) $(DSC)
+ lintian build/$(DSC)
+
+$(DSC): $(BUILDDIR)
+ cd $(BUILDDIR); dpkg-buildpackage -S -uc -us
+
+sbuild: $(DSC)
+ sbuild build/$(DSC)
+
+.PHONY: upload
+upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
+upload: $(DEBS)
+ tar cf - build/$(DEBS)|ssh repoman@repo.proxmox.com -- upload --product devel --dist $(UPLOAD_DIST)
+
+.PHONY: distclean
+distclean: clean
+
+.PHONY: clean
+clean:
+ rm -rf build
+
diff --git a/proxmox-frr-templates/debian/changelog b/proxmox-frr-templates/debian/changelog
new file mode 100644
index 000000000000..e70d0e12dfa8
--- /dev/null
+++ b/proxmox-frr-templates/debian/changelog
@@ -0,0 +1,5 @@
+proxmox-frr-templates (0.1.0-1) trixie; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 21 Jan 2026 15:53:51 +0100
diff --git a/proxmox-frr-templates/debian/control b/proxmox-frr-templates/debian/control
new file mode 100644
index 000000000000..d144d3f5f6b0
--- /dev/null
+++ b/proxmox-frr-templates/debian/control
@@ -0,0 +1,17 @@
+Source: proxmox-frr-templates
+Section: admin
+Priority: important
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Rules-Requires-Root: no
+Build-Depends: debhelper-compat (= 13)
+Vcs-Git: git://git.proxmox.com/git/proxmox-ve-rs.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox-ve-rs.git
+Homepage: https://proxmox.com
+
+Package: proxmox-frr-templates
+Architecture: all
+Depends: ${misc:Depends}
+Description: Provides template files which are used by the Proxmox VE SDN stack
+ to generate FRR config files.
+
diff --git a/proxmox-frr-templates/debian/copyright b/proxmox-frr-templates/debian/copyright
new file mode 100644
index 000000000000..1ea8a56b4f58
--- /dev/null
+++ b/proxmox-frr-templates/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-frr-templates/debian/proxmox-frr-templates.install b/proxmox-frr-templates/debian/proxmox-frr-templates.install
new file mode 100644
index 000000000000..a8e3df9d7260
--- /dev/null
+++ b/proxmox-frr-templates/debian/proxmox-frr-templates.install
@@ -0,0 +1 @@
+templates/* /usr/share/proxmox-frr/templates
diff --git a/proxmox-frr-templates/debian/rules b/proxmox-frr-templates/debian/rules
new file mode 100755
index 000000000000..abde6ef2247c
--- /dev/null
+++ b/proxmox-frr-templates/debian/rules
@@ -0,0 +1,5 @@
+#!/usr/bin/make -f
+
+%:
+ dh $@
+
diff --git a/proxmox-frr-templates/templates/access_list.jinja b/proxmox-frr-templates/templates/access_list.jinja
new file mode 100644
index 000000000000..5572fb9b0304
--- /dev/null
+++ b/proxmox-frr-templates/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 " ~ rule.seq ~ " ") if rule.seq }}{{ rule.action }} {{ rule.network }}
+{% endfor%}
+{% endfor %}
diff --git a/proxmox-frr-templates/templates/access_lists.jinja b/proxmox-frr-templates/templates/access_lists.jinja
new file mode 100644
index 000000000000..25f27a293529
--- /dev/null
+++ b/proxmox-frr-templates/templates/access_lists.jinja
@@ -0,0 +1,6 @@
+{% for access_list_name, access_list in access_lists | items %}
+!
+{% for rule in access_list %}
+{{ "ipv6 " if rule.is_ipv6 }}access-list {{ access_list_name }} {{ ("seq " ~ rule.seq ~ " ") if rule.seq }}{{ rule.action }} {{ rule.network }}
+{% endfor%}
+{% endfor %}
diff --git a/proxmox-frr-templates/templates/fabricd.jinja b/proxmox-frr-templates/templates/fabricd.jinja
new file mode 100644
index 000000000000..4857a24ebab4
--- /dev/null
+++ b/proxmox-frr-templates/templates/fabricd.jinja
@@ -0,0 +1,29 @@
+{% from "interface.jinja" import interface %}
+{% for router_name, router_config in openfabric.router|items %}
+!
+router openfabric {{ router_name }}
+ net {{ router_config.net }}
+exit
+{% endfor %}
+{% for interface_name, interface_config in openfabric.interfaces|items %}
+{% call interface(interface_name, interface_config.addresses) %}
+{% 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 %}
+{%- endcall %}
+{%- endfor %}
diff --git a/proxmox-frr-templates/templates/frr.conf.jinja b/proxmox-frr-templates/templates/frr.conf.jinja
new file mode 100644
index 000000000000..8458c6a61112
--- /dev/null
+++ b/proxmox-frr-templates/templates/frr.conf.jinja
@@ -0,0 +1,5 @@
+{% include "fabricd.jinja" %}
+{% include "ospfd.jinja" %}
+{% include "access_lists.jinja" %}
+{% include "route_maps.jinja" %}
+{% include "protocol_routemaps.jinja" %}
diff --git a/proxmox-frr-templates/templates/interface.jinja b/proxmox-frr-templates/templates/interface.jinja
new file mode 100644
index 000000000000..c3c3b6c3526f
--- /dev/null
+++ b/proxmox-frr-templates/templates/interface.jinja
@@ -0,0 +1,9 @@
+{% macro interface(name, addresses) %}
+!
+interface {{ name }}
+{% for address in addresses %}
+ ip address {{address}}
+{% endfor %}
+{{ caller() -}}
+exit
+{% endmacro %}
diff --git a/proxmox-frr-templates/templates/ospfd.jinja b/proxmox-frr-templates/templates/ospfd.jinja
new file mode 100644
index 000000000000..8f9e76390f75
--- /dev/null
+++ b/proxmox-frr-templates/templates/ospfd.jinja
@@ -0,0 +1,18 @@
+{% from "interface.jinja" import interface %}
+{% if ospf.router %}
+!
+router ospf
+ ospf router-id {{ ospf.router.router_id }}
+exit
+{% endif %}
+{% for interface_name, interface_config in ospf.interfaces|items %}
+{% call interface(interface_name, interface_config.addresses) %}
+ 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 %}
+{% endcall %}
+{% endfor %}
diff --git a/proxmox-frr-templates/templates/protocol_routemaps.jinja b/proxmox-frr-templates/templates/protocol_routemaps.jinja
new file mode 100644
index 000000000000..38c00b6407ee
--- /dev/null
+++ b/proxmox-frr-templates/templates/protocol_routemaps.jinja
@@ -0,0 +1,10 @@
+{% for protocol_name, protocol_routemap in protocol_routemaps | items %}
+!
+{% if protocol_routemap.v4 %}
+ip protocol {{ protocol_name }} route-map {{ protocol_routemap.v4 }}
+{% endif %}
+{% if protocol_routemap.v6 %}
+!
+ipv6 protocol {{ protocol_name }} route-map {{ protocol_routemap.v6 }}
+{% endif %}
+{% endfor %}
diff --git a/proxmox-frr-templates/templates/route_maps.jinja b/proxmox-frr-templates/templates/route_maps.jinja
new file mode 100644
index 000000000000..61fbf3256a19
--- /dev/null
+++ b/proxmox-frr-templates/templates/route_maps.jinja
@@ -0,0 +1,20 @@
+{% for name, routemap_list in routemaps | items %}
+{% for routemap in routemap_list %}
+!
+route-map {{ name }} {{ routemap.action }} {{ routemap.seq }}
+{% for match in routemap.matches %}
+{% if match.value.list_type == "prefixlist" %}
+ match {{ match.protocol_type }} {{ match.match_type }} prefix-list {{ match.value.list_name }}
+{% elif match.value.list_type == "accesslist" %}
+ match {{ match.protocol_type }} {{ match.match_type }} {{ match.value.list_name }}
+{% endif %}
+{% endfor %}
+{% for set in routemap.sets %}
+ set {{ set.set_type }} {{ set.value }}
+{% endfor %}
+{% for line in routemap.custom_frr_config %}
+{{ line }}
+{% endfor %}
+exit
+{% endfor %}
+{% endfor %}
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH proxmox-ve-rs v2 3/8] ve-config: remove FrrConfigBuilder struct
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 1/8] ve-config: firewall: cargo fmt Gabriel Goller
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 2/8] frr: add proxmox-frr-templates package that contains templates Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 4/8] sdn-types: support variable-length NET identifier Gabriel Goller
` (15 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
Instead of using the FrrConfigBuilder, build the frr config directly
using &mut FrrConfig. Update the tests accordingly.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-ve-config/src/sdn/frr.rs | 42 ----------
proxmox-ve-config/src/sdn/mod.rs | 2 -
proxmox-ve-config/tests/fabric/main.rs | 101 +++++++++++++++----------
3 files changed, 61 insertions(+), 84 deletions(-)
delete mode 100644 proxmox-ve-config/src/sdn/frr.rs
diff --git a/proxmox-ve-config/src/sdn/frr.rs b/proxmox-ve-config/src/sdn/frr.rs
deleted file mode 100644
index 5d4e4b2ebdbd..000000000000
--- a/proxmox-ve-config/src/sdn/frr.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use std::collections::{BTreeMap, BTreeSet};
-
-use proxmox_frr::ser::FrrConfig;
-
-use crate::common::valid::Valid;
-use crate::sdn::fabric::{section_config::node::NodeId, FabricConfig};
-
-/// Builder that helps constructing the FrrConfig.
-///
-/// The goal is to have one struct collect all the rust-based configurations and then construct the
-/// [`FrrConfig`] from it using the build method. In the future the controller configuration will
-/// be added here as well.
-#[derive(Default)]
-pub struct FrrConfigBuilder {
- fabrics: Valid<FabricConfig>,
-}
-
-impl FrrConfigBuilder {
- /// Add fabric configuration to the builder
- pub fn add_fabrics(mut self, fabric: Valid<FabricConfig>) -> FrrConfigBuilder {
- self.fabrics = fabric;
- self
- }
-
- /// Build the complete [`FrrConfig`] from this builder configuration given the hostname of the
- /// node for which we want to build the config. We also inject the common fabric-level options
- /// into the interfaces here. (e.g. the fabric-level "hello-interval" gets added to every
- /// 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(),
- };
-
- crate::sdn::fabric::frr::build_fabric(current_node, self.fabrics, &mut frr_config)?;
-
- Ok(frr_config)
- }
-}
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 4586c56358ef..86dcd938c92c 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -1,7 +1,5 @@
pub mod config;
pub mod fabric;
-#[cfg(feature = "frr")]
-pub mod frr;
pub mod ipam;
use std::{error::Error, fmt::Display, str::FromStr};
diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs
index 09629d406449..755592ff7482 100644
--- a/proxmox-ve-config/tests/fabric/main.rs
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -1,8 +1,7 @@
#![cfg(feature = "frr")]
-use proxmox_frr::ser::serializer::dump;
-use proxmox_ve_config::sdn::{
- fabric::{section_config::node::NodeId, FabricConfig},
- frr::FrrConfigBuilder,
+use proxmox_frr::ser::{serializer::dump, FrrConfig};
+use proxmox_ve_config::sdn::fabric::{
+ frr::build_fabric, section_config::node::NodeId, FabricConfig,
};
mod helper;
@@ -17,20 +16,25 @@ mod helper;
#[test]
fn openfabric_default() {
let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
-
- let mut frr_config = FrrConfigBuilder::default()
- .add_fabrics(config.clone())
- .build(NodeId::from_string("pve".to_owned()).expect("invalid nodeid"))
- .expect("error building frr config");
+ let mut frr_config = FrrConfig::default();
+ build_fabric(
+ NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+ config.clone(),
+ &mut frr_config,
+ )
+ .unwrap();
let mut output = dump(&frr_config).expect("error dumping stuff");
insta::assert_snapshot!(helper::reference_name!("pve"), output);
- frr_config = FrrConfigBuilder::default()
- .add_fabrics(config.clone())
- .build(NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"))
- .expect("error building frr config");
+ frr_config = FrrConfig::default();
+ build_fabric(
+ NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
+ config,
+ &mut frr_config,
+ )
+ .unwrap();
output = dump(&frr_config).expect("error dumping stuff");
@@ -40,20 +44,26 @@ fn openfabric_default() {
#[test]
fn ospf_default() {
let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+ let mut frr_config = FrrConfig::default();
- let mut frr_config = FrrConfigBuilder::default()
- .add_fabrics(config.clone())
- .build(NodeId::from_string("pve".to_owned()).expect("invalid nodeid"))
- .expect("error building frr config");
+ build_fabric(
+ NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+ config.clone(),
+ &mut frr_config,
+ )
+ .unwrap();
let mut output = dump(&frr_config).expect("error dumping stuff");
insta::assert_snapshot!(helper::reference_name!("pve"), output);
- frr_config = FrrConfigBuilder::default()
- .add_fabrics(config)
- .build(NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"))
- .expect("error building frr config");
+ frr_config = FrrConfig::default();
+ build_fabric(
+ NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
+ config,
+ &mut frr_config,
+ )
+ .unwrap();
output = dump(&frr_config).expect("error dumping stuff");
@@ -87,11 +97,14 @@ fn ospf_loopback_prefix_fail() {
#[test]
fn openfabric_multi_fabric() {
let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+ let mut frr_config = FrrConfig::default();
- let frr_config = FrrConfigBuilder::default()
- .add_fabrics(config)
- .build(NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"))
- .expect("error building frr config");
+ build_fabric(
+ NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
+ config,
+ &mut frr_config,
+ )
+ .unwrap();
let output = dump(&frr_config).expect("error dumping stuff");
@@ -101,12 +114,14 @@ fn openfabric_multi_fabric() {
#[test]
fn ospf_multi_fabric() {
let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
-
- let frr_config = FrrConfigBuilder::default()
- .add_fabrics(config)
- .build(NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"))
- .expect("error building frr config");
-
+ let mut frr_config = FrrConfig::default();
+
+ build_fabric(
+ NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
+ config,
+ &mut frr_config,
+ )
+ .unwrap();
let output = dump(&frr_config).expect("error dumping stuff");
insta::assert_snapshot!(helper::reference_name!("pve1"), output);
@@ -115,11 +130,14 @@ fn ospf_multi_fabric() {
#[test]
fn openfabric_dualstack() {
let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+ let mut frr_config = FrrConfig::default();
- let frr_config = FrrConfigBuilder::default()
- .add_fabrics(config)
- .build(NodeId::from_string("pve".to_owned()).expect("invalid nodeid"))
- .expect("error building frr config");
+ build_fabric(
+ NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+ config,
+ &mut frr_config,
+ )
+ .unwrap();
let output = dump(&frr_config).expect("error dumping stuff");
@@ -129,11 +147,14 @@ fn openfabric_dualstack() {
#[test]
fn openfabric_ipv6_only() {
let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
-
- let frr_config = FrrConfigBuilder::default()
- .add_fabrics(config)
- .build(NodeId::from_string("pve".to_owned()).expect("invalid nodeid"))
- .expect("error building frr config");
+ let mut frr_config = FrrConfig::default();
+
+ build_fabric(
+ NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+ config,
+ &mut frr_config,
+ )
+ .unwrap();
let output = dump(&frr_config).expect("error dumping stuff");
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH proxmox-ve-rs v2 4/8] sdn-types: support variable-length NET identifier
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (2 preceding siblings ...)
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 3/8] ve-config: remove FrrConfigBuilder struct Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 5/8] frr: add template serializer and serialize fabrics using templates Gabriel Goller
` (14 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
The NET (Network Entity Title) can actually be variable-length. We only
use the minimum length one (which corresponds to an ipv4 address) in the
fabrics, but in the ISIS tests we also use a longer NET. Support the
longer NET as well.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-sdn-types/src/net.rs | 136 +++++++++++++++++++++++++++++++----
1 file changed, 121 insertions(+), 15 deletions(-)
diff --git a/proxmox-sdn-types/src/net.rs b/proxmox-sdn-types/src/net.rs
index 3cd1e4f80ed7..3e523fb12d9b 100644
--- a/proxmox-sdn-types/src/net.rs
+++ b/proxmox-sdn-types/src/net.rs
@@ -10,7 +10,8 @@ use proxmox_schema::{api, api_string_type, const_regex, ApiStringFormat, Updater
const_regex! {
NET_AFI_REGEX = r"^(?:[a-fA-F0-9]{2})$";
- NET_AREA_REGEX = r"^(?:[a-fA-F0-9]{4})$";
+ // Variable length area: 0 to 13 bytes (0 to 26 hex digits) according to ISO 10589
+ NET_AREA_REGEX = r"^(?:[a-fA-F0-9]{0,26})$";
NET_SYSTEM_ID_REGEX = r"^(?:[a-fA-F0-9]{4})\.(?:[a-fA-F0-9]{4})\.(?:[a-fA-F0-9]{4})$";
NET_SELECTOR_REGEX = r"^(?:[a-fA-F0-9]{2})$";
}
@@ -39,9 +40,9 @@ impl UpdaterType for NetAFI {
}
api_string_type! {
- /// Area identifier: 0001 IS-IS area number (numerical area 1)
- /// The second part (system) of the `net` identifier. Every node has to have a different system
- /// number.
+ /// Area identifier: Variable length (0-13 bytes / 0-26 hex digits) according to ISO 10589
+ /// IS-IS area number that identifies the routing domain. All routers in the same area must
+ /// have the same area identifier. Can be empty or up to 26 hex digits.
#[api(format: &NET_AREA_FORMAT)]
#[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
struct NetArea(String);
@@ -146,6 +147,7 @@ pub struct Net {
selector: NetSelector,
}
+
impl UpdaterType for Net {
type Updater = Option<Net>;
}
@@ -156,27 +158,58 @@ impl std::str::FromStr for Net {
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split(".").collect();
- if parts.len() != 6 {
- bail!("invalid NET format: {s}")
+ // Minimum: AFI.SystemID(3 parts).Selector = 5 parts
+ // With area: AFI.Area.SystemID(3 parts).Selector = 6+ parts
+ if parts.len() < 5 {
+ bail!("invalid NET format: {s} (expected at least AFI.SystemID.Selector)")
}
- let system = format!("{}.{}.{}", parts[2], parts[3], parts[4],);
+ // Last part is selector (2 hex digits)
+ let selector_idx = parts.len() - 1;
+ let selector = parts[selector_idx];
+
+ // Three parts before selector are system ID (xxxx.xxxx.xxxx)
+ let system_id_parts = &parts[selector_idx - 3..selector_idx];
+ let system = format!(
+ "{}.{}.{}",
+ system_id_parts[0], system_id_parts[1], system_id_parts[2]
+ );
+
+ // First part is AFI (2 hex digits)
+ let afi = parts[0];
+
+ // Everything between AFI and system ID is the area (can be empty)
+ let area_parts = &parts[1..selector_idx - 3];
+ let area = area_parts.join("");
Ok(Self {
- afi: NetAFI::from_string(parts[0].to_string())?,
- area: NetArea::from_string(parts[1].to_string())?,
- system: NetSystemId::from_string(system.to_string())?,
- selector: NetSelector::from_string(parts[5].to_string())?,
+ afi: NetAFI::from_string(afi.to_string())?,
+ area: NetArea::from_string(area)?,
+ system: NetSystemId::from_string(system)?,
+ selector: NetSelector::from_string(selector.to_string())?,
})
}
}
impl Display for Net {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ // Format area with dots every 4 hex digits for readability
+ let area_str = self.area.0.as_str();
+ let area_formatted = if area_str.is_empty() {
+ String::new()
+ } else {
+ let chunks: Vec<&str> = area_str
+ .as_bytes()
+ .chunks(4)
+ .map(|chunk| std::str::from_utf8(chunk).unwrap())
+ .collect();
+ format!(".{}", chunks.join("."))
+ };
+
write!(
f,
- "{}.{}.{}.{}",
- self.afi, self.area, self.system, self.selector
+ "{}{}.{}.{}",
+ self.afi, area_formatted, self.system, self.selector
)
}
}
@@ -258,8 +291,16 @@ mod tests {
let input = "409.0001.1921.6800.1002.00";
input.parse::<Net>().expect_err("invalid AFI");
- let input = "49.00001.1921.6800.1002.00";
- input.parse::<Net>().expect_err("invalid area");
+ // Area can now be variable length (0-26 hex digits), so 5 digits is valid
+ // but 27 digits would be invalid
+ let input = "49.0123.4567.8901.2345.6789.0123.4569.1921.6800.1002.00";
+ input
+ .parse::<Net>()
+ .expect_err("area too long (>26 hex digits)");
+
+ // Too few parts
+ let input = "49.1921.6800.00";
+ input.parse::<Net>().expect_err("not enough parts");
}
#[test]
@@ -320,4 +361,69 @@ mod tests {
let net4: Net = ip4.into();
assert_eq!(format!("{net4}"), "49.0001.0000.0000.0000.00");
}
+
+ #[test]
+ fn test_net_variable_length_area() {
+ // Test with no area (just AFI)
+ let input = "49.1921.6800.1002.00";
+ let net = input.parse::<Net>().expect("should parse NET with no area");
+ assert_eq!(net.afi, NetAFI("49".to_owned()));
+ assert_eq!(net.area, NetArea("".to_owned()));
+ assert_eq!(net.system, NetSystemId("1921.6800.1002".to_owned()));
+ assert_eq!(net.selector, NetSelector("00".to_owned()));
+ assert_eq!(format!("{net}"), "49.1921.6800.1002.00");
+
+ // Test with 2 hex digit area
+ let input = "49.01.1921.6800.1002.00";
+ let net = input
+ .parse::<Net>()
+ .expect("should parse NET with 2-digit area");
+ assert_eq!(net.area, NetArea("01".to_owned()));
+ assert_eq!(format!("{net}"), "49.01.1921.6800.1002.00");
+
+ // Test with 4 hex digit area (standard)
+ let input = "49.0001.1921.6800.1002.00";
+ let net = input
+ .parse::<Net>()
+ .expect("should parse NET with 4-digit area");
+ assert_eq!(net.area, NetArea("0001".to_owned()));
+ assert_eq!(format!("{net}"), "49.0001.1921.6800.1002.00");
+
+ // Test with 8 hex digit area (formatted with dots every 4 digits)
+ let input = "49.0001.0002.1921.6800.1002.00";
+ let net = input
+ .parse::<Net>()
+ .expect("should parse NET with 8-digit area");
+ assert_eq!(net.area, NetArea("00010002".to_owned()));
+ // Should be formatted with dots every 4 hex digits
+ assert_eq!(format!("{net}"), "49.0001.0002.1921.6800.1002.00");
+
+ // Test with 12 hex digit area
+ let input = "49.0001.0002.0003.1921.6800.1002.00";
+ let net = input
+ .parse::<Net>()
+ .expect("should parse NET with 12-digit area");
+ assert_eq!(net.area, NetArea("000100020003".to_owned()));
+ assert_eq!(format!("{net}"), "49.0001.0002.0003.1921.6800.1002.00");
+
+ // Test with odd-length area (5 hex digits)
+ let input = "49.12345.1921.6800.1002.00";
+ let net = input
+ .parse::<Net>()
+ .expect("should parse NET with 5-digit area");
+ assert_eq!(net.area, NetArea("12345".to_owned()));
+ // Should be formatted with dots every 4 digits, last chunk has 1 digit
+ assert_eq!(format!("{net}"), "49.1234.5.1921.6800.1002.00");
+
+ // Test with maximum length area (26 hex digits = 13 bytes)
+ let input = "49.0123.4567.89ab.cdef.0123.4567.1921.6800.1002.00";
+ let net = input
+ .parse::<Net>()
+ .expect("should parse NET with 26-digit area");
+ assert_eq!(net.area, NetArea("0123456789abcdef01234567".to_owned()));
+ assert_eq!(
+ format!("{net}"),
+ "49.0123.4567.89ab.cdef.0123.4567.1921.6800.1002.00"
+ );
+ }
}
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH proxmox-ve-rs v2 5/8] frr: add template serializer and serialize fabrics using templates
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (3 preceding siblings ...)
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 4/8] sdn-types: support variable-length NET identifier Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 6/8] frr: add isis configuration and templates Gabriel Goller
` (13 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
Add a new serializer which uses only the builtin (include_str!) templates from
the `proxmox-frr-templates` package in `/usr/share/proxmox-frr/templates` to
generate the frr config file. Also update the `build_fabric` function and the
tests accordingly.
Use the `phf` crate to store them in a const map.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr/Cargo.toml | 3 +
proxmox-frr/debian/control | 12 +
proxmox-frr/src/ser/mod.rs | 260 +++++++---------
proxmox-frr/src/ser/openfabric.rs | 37 +--
proxmox-frr/src/ser/ospf.rs | 78 +----
proxmox-frr/src/ser/route_map.rs | 212 +++++--------
proxmox-frr/src/ser/serializer.rs | 227 ++------------
proxmox-sdn-types/src/net.rs | 4 +-
proxmox-ve-config/src/common/valid.rs | 4 +-
proxmox-ve-config/src/sdn/fabric/frr.rs | 284 ++++++++++--------
.../fabric__openfabric_default_pve.snap | 2 +-
.../fabric__openfabric_default_pve1.snap | 2 +-
.../fabric__openfabric_dualstack_pve.snap | 13 +-
.../fabric__openfabric_ipv6_only_pve.snap | 4 +-
.../fabric__openfabric_multi_fabric_pve1.snap | 2 +-
.../snapshots/fabric__ospf_default_pve.snap | 2 +-
.../snapshots/fabric__ospf_default_pve1.snap | 2 +-
.../fabric__ospf_multi_fabric_pve1.snap | 2 +-
18 files changed, 442 insertions(+), 708 deletions(-)
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
index 159f8606956d..d69eb63d967b 100644
--- a/proxmox-frr/Cargo.toml
+++ b/proxmox-frr/Cargo.toml
@@ -15,6 +15,9 @@ anyhow = "1"
tracing = "0.1"
serde = { workspace = true, features = [ "derive" ] }
serde_repr = "0.1"
+minijinja = { version = "2.5", features = [ "multi_template", "loader" ] }
+phf = { version = "0.11.2", features = ["macros"] }
proxmox-network-types = { workspace = true }
proxmox-sdn-types = { workspace = true }
+proxmox-serde = { workspace = true }
diff --git a/proxmox-frr/debian/control b/proxmox-frr/debian/control
index bc407807ad4f..427a5fccc59d 100644
--- a/proxmox-frr/debian/control
+++ b/proxmox-frr/debian/control
@@ -7,8 +7,14 @@ Build-Depends-Arch: cargo:native <!nocheck>,
rustc:native (>= 1.82) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
+ librust-minijinja-2+default-dev (>= 2.5-~~) <!nocheck>,
+ librust-minijinja-2+loader-dev (>= 2.5-~~) <!nocheck>,
+ librust-minijinja-2+multi-template-dev (>= 2.5-~~) <!nocheck>,
+ librust-phf-0.11+default-dev (>= 0.11.2-~~) <!nocheck>,
+ librust-phf-0.11+macros-dev (>= 0.11.2-~~) <!nocheck>,
librust-proxmox-network-types-1+default-dev (>= 1.0.1-~~) <!nocheck>,
librust-proxmox-sdn-types-0.1+default-dev <!nocheck>,
+ librust-proxmox-serde-1+default-dev <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>,
librust-serde-repr-0.1+default-dev <!nocheck>,
@@ -27,8 +33,14 @@ Multi-Arch: same
Depends:
${misc:Depends},
librust-anyhow-1+default-dev,
+ librust-minijinja-2+default-dev (>= 2.5-~~),
+ librust-minijinja-2+loader-dev (>= 2.5-~~),
+ librust-minijinja-2+multi-template-dev (>= 2.5-~~),
+ librust-phf-0.11+default-dev (>= 0.11.2-~~),
+ librust-phf-0.11+macros-dev (>= 0.11.2-~~),
librust-proxmox-network-types-1+default-dev (>= 1.0.1-~~),
librust-proxmox-sdn-types-0.1+default-dev,
+ librust-proxmox-serde-1+default-dev,
librust-serde-1+default-dev,
librust-serde-1+derive-dev,
librust-serde-repr-0.1+default-dev,
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
index a90397b59a9b..86813c7d6415 100644
--- a/proxmox-frr/src/ser/mod.rs
+++ b/proxmox-frr/src/ser/mod.rs
@@ -3,104 +3,16 @@ pub mod ospf;
pub mod route_map;
pub mod serializer;
-use std::collections::{BTreeMap, BTreeSet};
-use std::fmt::Display;
+use std::collections::BTreeMap;
+use std::net::IpAddr;
use std::str::FromStr;
-use crate::ser::route_map::{AccessList, ProtocolRouteMap, RouteMap};
+use crate::ser::route_map::{AccessListName, AccessListRule, RouteMapEntry, RouteMapName};
+use proxmox_network_types::ip_address::Cidr;
+use serde::{Deserialize, Serialize};
use thiserror::Error;
-/// Generic FRR router.
-///
-/// This generic FRR router contains all the protocols that we implement.
-/// In FRR this is e.g.:
-/// ```text
-/// router openfabric test
-/// !....
-/// ! or
-/// router ospf
-/// !....
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum Router {
- Openfabric(openfabric::OpenfabricRouter),
- Ospf(ospf::OspfRouter),
-}
-
-impl From<openfabric::OpenfabricRouter> for Router {
- fn from(value: openfabric::OpenfabricRouter) -> Self {
- Router::Openfabric(value)
- }
-}
-
-/// Generic FRR routername.
-///
-/// The variants represent different protocols. Some have `router <protocol> <name>`, others have
-/// `router <protocol> <process-id>`, some only have `router <protocol>`.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum RouterName {
- Openfabric(openfabric::OpenfabricRouterName),
- Ospf(ospf::OspfRouterName),
-}
-
-impl From<openfabric::OpenfabricRouterName> for RouterName {
- fn from(value: openfabric::OpenfabricRouterName) -> Self {
- Self::Openfabric(value)
- }
-}
-
-impl Display for RouterName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Openfabric(r) => r.fmt(f),
- Self::Ospf(r) => r.fmt(f),
- }
- }
-}
-
-/// The interface name is the same on ospf and openfabric, but it is an enum so that we can have
-/// two different entries in the btreemap. This allows us to have an interface in a ospf and
-/// openfabric fabric.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum InterfaceName {
- Openfabric(CommonInterfaceName),
- Ospf(CommonInterfaceName),
-}
-
-impl Display for InterfaceName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- InterfaceName::Openfabric(frr_word) => frr_word.fmt(f),
- InterfaceName::Ospf(frr_word) => frr_word.fmt(f),
- }
- }
-}
-
-/// Generic FRR Interface.
-///
-/// In FRR config it looks like this:
-/// ```text
-/// interface <name>
-/// ! ...
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
-pub enum Interface {
- Openfabric(openfabric::OpenfabricInterface),
- Ospf(ospf::OspfInterface),
-}
-
-impl From<openfabric::OpenfabricInterface> for Interface {
- fn from(value: openfabric::OpenfabricInterface) -> Self {
- Self::Openfabric(value)
- }
-}
-
-impl From<ospf::OspfInterface> for Interface {
- fn from(value: ospf::OspfInterface) -> Self {
- Self::Ospf(value)
- }
-}
-
#[derive(Error, Debug)]
pub enum FrrWordError {
#[error("word is empty")]
@@ -113,7 +25,7 @@ pub enum FrrWordError {
///
/// Every string argument or value in FRR is an FrrWord. FrrWords must only contain ascii
/// characters and must not have a whitespace.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct FrrWord(String);
impl FrrWord {
@@ -144,12 +56,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 +63,7 @@ impl AsRef<str> for FrrWord {
}
#[derive(Error, Debug)]
-pub enum CommonInterfaceNameError {
+pub enum InterfaceNameError {
#[error("interface name too long")]
TooLong,
}
@@ -166,76 +72,132 @@ pub enum CommonInterfaceNameError {
///
/// FRR itself doesn't enforce any limits, but the kernel does. Linux only allows interface names
/// to be a maximum of 16 bytes. This is enforced by this struct.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct CommonInterfaceName(String);
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct InterfaceName(String);
-impl TryFrom<&str> for CommonInterfaceName {
- type Error = CommonInterfaceNameError;
+impl TryFrom<&str> for InterfaceName {
+ type Error = InterfaceNameError;
- fn try_from(value: &str) -> Result<Self, Self::Error> {
- Self::new(value)
+ fn try_from(s: &str) -> Result<Self, Self::Error> {
+ Self::validate(s).map(Self::from_str_unchecked)
}
}
-impl TryFrom<String> for CommonInterfaceName {
- type Error = CommonInterfaceNameError;
+impl TryFrom<String> for InterfaceName {
+ type Error = InterfaceNameError;
fn try_from(value: String) -> Result<Self, Self::Error> {
- Self::new(value)
+ if Self::validate(&value).is_ok() {
+ Ok(Self::from_string_unchecked(value))
+ } else {
+ Err(InterfaceNameError::TooLong)
+ }
}
}
-impl CommonInterfaceName {
- pub fn new<T: AsRef<str> + Into<String>>(s: T) -> Result<Self, CommonInterfaceNameError> {
- if s.as_ref().len() <= 15 {
- Ok(Self(s.into()))
+impl InterfaceName {
+ fn validate(s: &str) -> Result<&str, InterfaceNameError> {
+ if s.len() <= 15 {
+ Ok(s)
} else {
- Err(CommonInterfaceNameError::TooLong)
+ Err(InterfaceNameError::TooLong)
}
}
-}
+ fn from_string_unchecked(s: String) -> InterfaceName {
+ Self(s)
+ }
-impl Display for CommonInterfaceName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
+ fn from_str_unchecked(s: &str) -> InterfaceName {
+ Self::from_string_unchecked(s.to_string())
}
}
-/// Main FRR config.
-///
-/// Contains the two main frr building blocks: routers and interfaces. It also holds other
-/// top-level FRR options, such as access-lists, router-maps and protocol-routemaps. This struct
-/// gets generated using the `FrrConfigBuilder` in `proxmox-ve-config`.
-#[derive(Clone, Debug, PartialEq, Eq, Default)]
-pub struct FrrConfig {
- pub router: BTreeMap<RouterName, Router>,
- pub interfaces: BTreeMap<InterfaceName, Interface>,
- pub access_lists: Vec<AccessList>,
- pub routemaps: Vec<RouteMap>,
- pub protocol_routemaps: BTreeSet<ProtocolRouteMap>,
-}
+#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)]
+pub struct Interface<T> {
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub addresses: Vec<Cidr>,
-impl FrrConfig {
- pub fn new() -> Self {
- Self::default()
+ #[serde(flatten)]
+ pub properties: T,
+}
+impl From<openfabric::OpenfabricInterface> for Interface<openfabric::OpenfabricInterface> {
+ fn from(value: openfabric::OpenfabricInterface) -> Self {
+ Interface {
+ addresses: Vec::new(),
+ properties: value,
+ }
}
+}
- pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
- self.router.iter()
+impl From<ospf::OspfInterface> for Interface<ospf::OspfInterface> {
+ fn from(value: ospf::OspfInterface) -> Self {
+ Interface {
+ addresses: Vec::new(),
+ properties: value,
+ }
}
+}
- pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
- self.interfaces.iter()
- }
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum IpOrInterface {
+ Ip(IpAddr),
+ Interface(InterfaceName),
+}
- pub fn access_lists(&self) -> impl Iterator<Item = &AccessList> + '_ {
- self.access_lists.iter()
- }
- pub fn routemaps(&self) -> impl Iterator<Item = &RouteMap> + '_ {
- self.routemaps.iter()
- }
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct IpRoute {
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ is_ipv6: bool,
+ prefix: Cidr,
+ via: IpOrInterface,
+ vrf: Option<InterfaceName>,
+}
- pub fn protocol_routemaps(&self) -> impl Iterator<Item = &ProtocolRouteMap> + '_ {
- self.protocol_routemaps.iter()
- }
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum FrrProtocol {
+ Ospf,
+ Openfabric,
+ Bgp,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct IpProtocolRouteMap {
+ pub v4: Option<RouteMapName>,
+ pub v6: Option<RouteMapName>,
+}
+
+/// Main FRR config.
+///
+/// Contains the two main frr building blocks: routers and interfaces. It also holds other
+/// top-level FRR options, such as access-lists, router-maps and protocol-routemaps.
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub struct FrrConfig {
+ #[serde(default)]
+ pub openfabric: OpenfabricFrrConfig,
+ #[serde(default)]
+ pub ospf: OspfFrrConfig,
+ #[serde(default)]
+ pub protocol_routemaps: BTreeMap<FrrProtocol, IpProtocolRouteMap>,
+ #[serde(default)]
+ pub routemaps: BTreeMap<RouteMapName, Vec<RouteMapEntry>>,
+ #[serde(default)]
+ pub access_lists: BTreeMap<AccessListName, Vec<AccessListRule>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub struct OpenfabricFrrConfig {
+ #[serde(default)]
+ pub router: BTreeMap<openfabric::OpenfabricRouterName, openfabric::OpenfabricRouter>,
+ #[serde(default)]
+ pub interfaces: BTreeMap<InterfaceName, Interface<openfabric::OpenfabricInterface>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub struct OspfFrrConfig {
+ #[serde(default)]
+ pub router: Option<ospf::OspfRouter>,
+ #[serde(default)]
+ pub interfaces: BTreeMap<InterfaceName, Interface<ospf::OspfInterface>>,
}
diff --git a/proxmox-frr/src/ser/openfabric.rs b/proxmox-frr/src/ser/openfabric.rs
index 0f0c65062d36..58d55da285b1 100644
--- a/proxmox-frr/src/ser/openfabric.rs
+++ b/proxmox-frr/src/ser/openfabric.rs
@@ -1,15 +1,16 @@
use std::fmt::Debug;
-use std::fmt::Display;
use proxmox_sdn_types::net::Net;
+use serde::Deserialize;
+use serde::Serialize;
use thiserror::Error;
use crate::ser::FrrWord;
use crate::ser::FrrWordError;
/// The name of a OpenFabric router. Is an FrrWord.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct OpenfabricRouterName(FrrWord);
impl From<FrrWord> for OpenfabricRouterName {
@@ -24,16 +25,16 @@ impl OpenfabricRouterName {
}
}
-impl Display for OpenfabricRouterName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "openfabric {}", self.0)
+impl std::fmt::Display for OpenfabricRouterName {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ self.0.fmt(f)
}
}
/// All the properties a OpenFabric router can hold.
///
/// These can serialized with a " " space prefix as they are in the `router openfabric` block.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct OpenfabricRouter {
/// The NET address
pub net: Net,
@@ -53,29 +54,29 @@ impl OpenfabricRouter {
///
/// This struct holds all the OpenFabric interface properties. The most important one here is the
/// fabric_id, which ties the interface to a fabric. When serialized these properties all get
-/// prefixed with a space (" ") as they are inside the interface block. They serialize roughly to:
-///
-/// ```text
-/// interface ens20
-/// ip router openfabric <fabric_id>
-/// ipv6 router openfabric <fabric_id>
-/// openfabric hello-interval <value>
-/// openfabric hello-multiplier <value>
-/// openfabric csnp-interval <value>
-/// openfabric passive <value>
-/// ```
+/// prefixed with a space (" ") as they are inside the interface block.
///
/// The is_ipv4 and is_ipv6 properties decide if we need to add `ip router openfabric`, `ipv6
/// router openfabric`, or both. An interface can only be part of a single fabric.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct OpenfabricInterface {
// Note: an interface can only be a part of a single fabric (so no vec needed here)
pub fabric_id: OpenfabricRouterName,
+ #[serde(
+ default,
+ deserialize_with = "proxmox_serde::perl::deserialize_bool",
+ skip_serializing_if = "Option::is_none"
+ )]
pub passive: Option<bool>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub hello_interval: Option<proxmox_sdn_types::openfabric::HelloInterval>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub csnp_interval: Option<proxmox_sdn_types::openfabric::CsnpInterval>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub hello_multiplier: Option<proxmox_sdn_types::openfabric::HelloMultiplier>,
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
pub is_ipv4: bool,
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
pub is_ipv6: bool,
}
diff --git a/proxmox-frr/src/ser/ospf.rs b/proxmox-frr/src/ser/ospf.rs
index 67e39a45b8de..8c9992e901f2 100644
--- a/proxmox-frr/src/ser/ospf.rs
+++ b/proxmox-frr/src/ser/ospf.rs
@@ -1,32 +1,11 @@
use std::fmt::Debug;
-use std::fmt::Display;
use std::net::Ipv4Addr;
+use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::ser::{FrrWord, FrrWordError};
-/// The name of the ospf frr router.
-///
-/// We can only have a single ospf router (ignoring multiple invocations of the ospfd daemon)
-/// because the router-id needs to be the same between different routers on a single node.
-/// We can still have multiple fabrics by separating them using areas. Still, different areas have
-/// the same frr router, so the name of the router is just "ospf" in "router ospf".
-///
-/// This serializes roughly to:
-/// ```text
-/// router ospf
-/// !...
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct OspfRouterName;
-
-impl Display for OspfRouterName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "ospf")
- }
-}
-
#[derive(Error, Debug)]
pub enum AreaParsingError {
#[error("Invalid area idenitifier. Area must be a number or an ipv4 address.")]
@@ -44,7 +23,7 @@ pub enum AreaParsingError {
/// or "0" as an area, which then gets translated to "0.0.0.5" and "0.0.0.0" by FRR. We allow both
/// a number or an ip-address. Note that the area "0" (or "0.0.0.0") is a special area - it creates
/// a OSPF "backbone" area.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Area(FrrWord);
impl TryFrom<FrrWord> for Area {
@@ -65,26 +44,13 @@ impl Area {
}
}
-impl Display for Area {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "area {}", self.0)
- }
-}
-
/// The OSPF router properties.
///
/// Currently the only property of a OSPF router is the router_id. The router_id is used to
/// differentiate between nodes and every node in the same area must have a different router_id.
/// The router_id must also be the same on the different fabrics on the same node. The OSPFv2
/// daemon only supports IPv4.
-/// Note that these properties also serialize with a space prefix (" ") as they are inside the OSPF
-/// router block. It serializes roughly to:
-///
-/// ```text
-/// router ospf
-/// router-id <ipv4-address>
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct OspfRouter {
pub router_id: Ipv4Addr,
}
@@ -112,14 +78,8 @@ pub enum OspfInterfaceError {
/// The most important options here are Broadcast (which is the default) and PointToPoint.
/// When PointToPoint is set, then the interface has to have a /32 address and will be treated as
/// unnumbered.
-///
-/// This roughly serializes to:
-/// ```text
-/// ip ospf network point-to-point
-/// ! or
-/// ip ospf network broadcast
-/// ```
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
pub enum NetworkType {
Broadcast,
NonBroadcast,
@@ -132,34 +92,20 @@ pub enum NetworkType {
PointToMultipoint,
}
-impl Display for NetworkType {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- NetworkType::Broadcast => write!(f, "broadcast"),
- NetworkType::NonBroadcast => write!(f, "non-broadcast"),
- NetworkType::PointToPoint => write!(f, "point-to-point"),
- NetworkType::PointToMultipoint => write!(f, "point-to-multicast"),
- }
- }
-}
-
/// The OSPF interface properties.
///
/// The interface gets tied to its fabric by the area property and the FRR `ip ospf area <area>`
/// command.
-///
-/// This serializes to:
-///
-/// ```text
-/// router ospf
-/// ip ospf area <area>
-/// ip ospf passive <value>
-/// ip ospf network <value>
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct OspfInterface {
// Note: an interface can only be a part of a single area(so no vec needed here)
pub area: Area,
+ #[serde(
+ default,
+ skip_serializing_if = "Option::is_none",
+ deserialize_with = "proxmox_serde::perl::deserialize_bool"
+ )]
pub passive: Option<bool>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub network_type: Option<NetworkType>,
}
diff --git a/proxmox-frr/src/ser/route_map.rs b/proxmox-frr/src/ser/route_map.rs
index 0918a3cead14..3e13b46c5d7b 100644
--- a/proxmox-frr/src/ser/route_map.rs
+++ b/proxmox-frr/src/ser/route_map.rs
@@ -1,29 +1,19 @@
-use std::{
- fmt::{self, Display},
- net::IpAddr,
-};
+use std::net::IpAddr;
use proxmox_network_types::ip_address::Cidr;
+use serde::{Deserialize, Serialize};
/// The action for a [`AccessListRule`].
///
/// The default is Permit. Deny can be used to create a NOT match (e.g. match all routes that are
/// NOT in 10.10.10.0/24 using `ip access-list TEST deny 10.10.10.0/24`).
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
pub enum AccessAction {
Permit,
Deny,
}
-impl fmt::Display for AccessAction {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- AccessAction::Permit => write!(f, "permit"),
- AccessAction::Deny => write!(f, "deny"),
- }
- }
-}
-
/// A single [`AccessList`] rule.
///
/// Every rule in a [`AccessList`] is its own command and gets written into a new line (with the
@@ -32,132 +22,113 @@ impl fmt::Display for AccessAction {
/// between access-lists of the same name and rules. Every [`AccessListRule`] has to have a
/// different seq number.
/// The `ip` or `ipv6` prefix gets decided based on the Cidr address passed.
-///
-/// This serializes to:
-///
-/// ```text
-/// ip access-list filter permit 10.0.0.0/8
-/// ! or
-/// ipv6 access-list filter permit 2001:db8::/64
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccessListRule {
pub action: AccessAction,
pub network: Cidr,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub seq: Option<u32>,
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub is_ipv6: bool,
}
/// The name of an [`AccessList`].
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct AccessListName(String);
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct PrefixListName(String);
+
impl AccessListName {
pub fn new(name: String) -> AccessListName {
AccessListName(name)
}
}
-impl Display for AccessListName {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
- }
-}
-
/// A FRR access-list.
///
/// Holds a vec of rules. Each rule will get its own line, FRR will collect all the rules with the
/// same name and combine them.
-///
-/// This serializes to:
-///
-/// ```text
-/// ip access-list pve_test permit 10.0.0.0/24
-/// ip access-list pve_test permit 12.1.1.0/24
-/// ip access-list pve_test deny 8.8.8.8/32
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccessList {
pub name: AccessListName,
pub rules: Vec<AccessListRule>,
}
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct PrefixList {
+ pub name: PrefixListName,
+ pub rules: Vec<PrefixListRule>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct PrefixListRule {
+ pub action: AccessAction,
+ pub network: Cidr,
+ pub seq: Option<u32>,
+ pub le: Option<u32>,
+ pub ge: Option<u32>,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub is_ipv6: bool,
+}
+
/// A match statement inside a route-map.
///
/// A route-map has one or more match statements which decide on which routes the route-map will
/// execute its actions. If we match on an IP, there are two different syntaxes: `match ip ...` or
/// `match ipv6 ...`.
-///
-/// Serializes to:
-///
-/// ```text
-/// match ip address <access-list-name>
-/// ! or
-/// match ip next-hop <ip-address>
-/// ! or
-/// match ipv6 address <access-list-name>
-/// ! or
-/// match ipv6 next-hop <ip-address>
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(tag = "protocol_type")]
pub enum RouteMapMatch {
+ #[serde(rename = "ip")]
V4(RouteMapMatchInner),
+ #[serde(rename = "ipv6")]
V6(RouteMapMatchInner),
+ Vni(u32),
}
-impl Display for RouteMapMatch {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- RouteMapMatch::V4(route_map_match_v4) => match route_map_match_v4 {
- RouteMapMatchInner::IpAddress(access_list_name) => {
- write!(f, "match ip address {access_list_name}")
- }
- RouteMapMatchInner::IpNextHop(next_hop) => {
- write!(f, "match ip next-hop {next_hop}")
- }
- },
- RouteMapMatch::V6(route_map_match_v6) => match route_map_match_v6 {
- RouteMapMatchInner::IpAddress(access_list_name) => {
- write!(f, "match ipv6 address {access_list_name}")
- }
- RouteMapMatchInner::IpNextHop(next_hop) => {
- write!(f, "match ipv6 next-hop {next_hop}")
- }
- },
- }
- }
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(tag = "list_type", content = "list_name", rename_all = "lowercase")]
+pub enum AccessListOrPrefixList {
+ PrefixList(PrefixListName),
+ AccessList(AccessListName),
}
/// A route-map match statement generic on the IP-version.
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(tag = "match_type", content = "value", rename_all = "kebab-case")]
pub enum RouteMapMatchInner {
- IpAddress(AccessListName),
- IpNextHop(String),
+ Address(AccessListOrPrefixList),
+ NextHop(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum SetIpNextHopValue {
+ PeerAddress,
+ Unchanged,
+ IpAddr(IpAddr),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum SetTagValue {
+ Untagged,
+ Numeric(u32),
}
/// Defines the Action a route-map takes when it matches on a route.
///
/// If the route matches the [`RouteMapMatch`], then a [`RouteMapSet`] action will be executed.
/// We currently only use the IpSrc command which changes the source address of the route.
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(tag = "set_type", content = "value", rename_all = "kebab-case")]
pub enum RouteMapSet {
LocalPreference(u32),
- IpSrc(IpAddr),
+ Src(IpAddr),
Metric(u32),
Community(String),
}
-impl Display for RouteMapSet {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- RouteMapSet::LocalPreference(pref) => write!(f, "set local-preference {}", pref),
- RouteMapSet::IpSrc(addr) => write!(f, "set src {}", addr),
- RouteMapSet::Metric(metric) => write!(f, "set metric {}", metric),
- RouteMapSet::Community(community) => write!(f, "set community {}", community),
- }
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct RouteMapName(String);
impl RouteMapName {
@@ -166,68 +137,19 @@ impl RouteMapName {
}
}
-impl Display for RouteMapName {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
- }
-}
-
/// A FRR route-map.
///
/// In FRR route-maps are used to manipulate routes learned by protocols. We can match on specific
/// routes (from specific protocols or subnets) and then change them, by e.g. editing the source
/// address or adding a metric, bgp community, or local preference.
-///
-/// This serializes to:
-///
-/// ```text
-/// route-map <name> permit 100
-/// match ip address <access-list>
-/// set src <ip-address>
-/// exit
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RouteMap {
- pub name: RouteMapName,
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RouteMapEntry {
pub seq: u32,
pub action: AccessAction,
+ #[serde(default)]
pub matches: Vec<RouteMapMatch>,
+ #[serde(default)]
pub sets: Vec<RouteMapSet>,
-}
-
-/// The ProtocolType used in the [`ProtocolRouteMap`].
-///
-/// Specifies to which protocols we can attach route-maps.
-#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum ProtocolType {
- Openfabric,
- Ospf,
-}
-
-impl Display for ProtocolType {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- ProtocolType::Openfabric => write!(f, "openfabric"),
- ProtocolType::Ospf => write!(f, "ospf"),
- }
- }
-}
-
-/// ProtocolRouteMap statement.
-///
-/// This statement attaches the route-map to the protocol, so that all the routes learned through
-/// the specified protocol can be matched on and manipulated with the route-map.
-///
-/// This serializes to:
-///
-/// ```text
-/// ip protocol <protocol> route-map <route-map-name>
-/// ! or
-/// ipv6 protocol <protocol> route-map <route-map-name>
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct ProtocolRouteMap {
- pub is_ipv6: bool,
- pub protocol: ProtocolType,
- pub routemap_name: RouteMapName,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub custom_frr_config: Vec<String>,
}
diff --git a/proxmox-frr/src/ser/serializer.rs b/proxmox-frr/src/ser/serializer.rs
index 3a681e2f0d7a..51c46729c8ce 100644
--- a/proxmox-frr/src/ser/serializer.rs
+++ b/proxmox-frr/src/ser/serializer.rs
@@ -1,203 +1,42 @@
-use std::fmt::{self, Write};
-
-use crate::ser::{
- openfabric::{OpenfabricInterface, OpenfabricRouter},
- ospf::{OspfInterface, OspfRouter},
- route_map::{AccessList, AccessListName, ProtocolRouteMap, RouteMap},
- FrrConfig, Interface, InterfaceName, Router, RouterName,
+use anyhow::Context;
+use minijinja::Environment;
+
+use crate::ser::FrrConfig;
+
+pub static TEMPLATES: phf::Map<&'static str, &'static str> = phf::phf_map! {
+ "fabricd.jinja" => include_str!("/usr/share/proxmox-frr/templates/fabricd.jinja"),
+ "ospfd.jinja" => include_str!("/usr/share/proxmox-frr/templates/ospfd.jinja"),
+ "interface.jinja" => include_str!("/usr/share/proxmox-frr/templates/interface.jinja"),
+ "access_lists.jinja" => include_str!("/usr/share/proxmox-frr/templates/access_lists.jinja"),
+ "route_maps.jinja" => include_str!("/usr/share/proxmox-frr/templates/route_maps.jinja"),
+ "protocol_routemaps.jinja" => include_str!("/usr/share/proxmox-frr/templates/protocol_routemaps.jinja"),
+ "frr.conf.jinja" => include_str!("/usr/share/proxmox-frr/templates/frr.conf.jinja"),
};
-pub struct FrrConfigBlob<'a> {
- buf: &'a mut (dyn Write + 'a),
-}
-
-impl Write for FrrConfigBlob<'_> {
- fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
- self.buf.write_str(s)
- }
-}
+fn create_env<'a>() -> Environment<'a> {
+ let mut env = Environment::new();
-pub trait FrrSerializer {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result;
-}
+ // avoid unnecessary additional newlines
+ env.set_trim_blocks(true);
+ env.set_lstrip_blocks(true);
-pub fn to_raw_config(frr_config: &FrrConfig) -> Result<Vec<String>, anyhow::Error> {
- let mut out = String::new();
- let mut blob = FrrConfigBlob { buf: &mut out };
- frr_config.serialize(&mut blob)?;
+ env.set_loader(move |name| Ok(TEMPLATES.get(name).map(|template| (*template).to_owned())));
- Ok(out.as_str().lines().map(String::from).collect())
+ env
}
+/// Render the passed [`FrrConfig`] into a single string containing the whole config.
pub fn dump(config: &FrrConfig) -> Result<String, anyhow::Error> {
- let mut out = String::new();
- let mut blob = FrrConfigBlob { buf: &mut out };
- config.serialize(&mut blob)?;
- Ok(out)
-}
-
-impl FrrSerializer for FrrConfig {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- self.router().try_for_each(|router| router.serialize(f))?;
- self.interfaces()
- .try_for_each(|interface| interface.serialize(f))?;
- self.access_lists().try_for_each(|list| list.serialize(f))?;
- self.routemaps().try_for_each(|map| map.serialize(f))?;
- self.protocol_routemaps()
- .try_for_each(|pm| pm.serialize(f))?;
- Ok(())
- }
-}
-
-impl FrrSerializer for (&RouterName, &Router) {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- let router_name = self.0;
- let router = self.1;
- writeln!(f, "router {router_name}")?;
- router.serialize(f)?;
- writeln!(f, "exit")?;
- writeln!(f, "!")?;
- Ok(())
- }
-}
-
-impl FrrSerializer for (&InterfaceName, &Interface) {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- let interface_name = self.0;
- let interface = self.1;
- writeln!(f, "interface {interface_name}")?;
- interface.serialize(f)?;
- writeln!(f, "exit")?;
- writeln!(f, "!")?;
- Ok(())
- }
-}
-
-impl FrrSerializer for (&AccessListName, &AccessList) {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- self.1.serialize(f)?;
- writeln!(f, "!")
- }
-}
-
-impl FrrSerializer for Interface {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- match self {
- Interface::Openfabric(openfabric_interface) => openfabric_interface.serialize(f)?,
- Interface::Ospf(ospf_interface) => ospf_interface.serialize(f)?,
- }
- Ok(())
- }
-}
-
-impl FrrSerializer for OpenfabricInterface {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- if self.is_ipv6 {
- writeln!(f, " ipv6 router {}", self.fabric_id)?;
- }
- if self.is_ipv4 {
- writeln!(f, " ip router {}", self.fabric_id)?;
- }
- if self.passive == Some(true) {
- writeln!(f, " openfabric passive")?;
- }
- if let Some(interval) = self.hello_interval {
- writeln!(f, " openfabric hello-interval {interval}",)?;
- }
- if let Some(multiplier) = self.hello_multiplier {
- writeln!(f, " openfabric hello-multiplier {multiplier}",)?;
- }
- if let Some(interval) = self.csnp_interval {
- writeln!(f, " openfabric csnp-interval {interval}",)?;
- }
- Ok(())
- }
-}
-
-impl FrrSerializer for OspfInterface {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- writeln!(f, " ip ospf {}", self.area)?;
- if self.passive == Some(true) {
- writeln!(f, " ip ospf passive")?;
- }
- if let Some(network_type) = &self.network_type {
- writeln!(f, " ip ospf network {network_type}")?;
- }
- Ok(())
- }
-}
-
-impl FrrSerializer for Router {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- match self {
- Router::Openfabric(open_fabric_router) => open_fabric_router.serialize(f),
- Router::Ospf(ospf_router) => ospf_router.serialize(f),
- }
- }
-}
-
-impl FrrSerializer for OpenfabricRouter {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- writeln!(f, " net {}", self.net())?;
- Ok(())
- }
-}
-
-impl FrrSerializer for OspfRouter {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- writeln!(f, " ospf router-id {}", self.router_id())?;
- Ok(())
- }
-}
-
-impl FrrSerializer for AccessList {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- for i in &self.rules {
- if i.network.is_ipv6() {
- write!(f, "ipv6 ")?;
- }
- write!(f, "access-list {} ", self.name)?;
- if let Some(seq) = i.seq {
- write!(f, "seq {seq} ")?;
- }
- write!(f, "{} ", i.action)?;
- writeln!(f, "{}", i.network)?;
- }
- writeln!(f, "!")?;
- Ok(())
- }
-}
-
-impl FrrSerializer for RouteMap {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- writeln!(f, "route-map {} {} {}", self.name, self.action, self.seq)?;
- for i in &self.matches {
- writeln!(f, " {}", i)?;
- }
- for i in &self.sets {
- writeln!(f, " {}", i)?;
- }
- writeln!(f, "exit")?;
- writeln!(f, "!")
- }
-}
-
-impl FrrSerializer for ProtocolRouteMap {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- if self.is_ipv6 {
- writeln!(
- f,
- "ipv6 protocol {} route-map {}",
- self.protocol, self.routemap_name
- )?;
- } else {
- writeln!(
- f,
- "ip protocol {} route-map {}",
- self.protocol, self.routemap_name
- )?;
- }
- writeln!(f, "!")?;
- Ok(())
- }
+ create_env()
+ .get_template("frr.conf.jinja")
+ .with_context(|| "could not obtain frr template from environment")?
+ .render(config)
+ .with_context(|| "could not render frr template")
+}
+
+/// Render the passed [`FrrConfig`] into the literal Frr config.
+///
+/// The Frr config is returned as lines stored in a Vec.
+pub fn to_raw_config(config: &FrrConfig) -> Result<Vec<String>, anyhow::Error> {
+ Ok(dump(config)?.lines().map(|line| line.to_owned()).collect())
}
diff --git a/proxmox-sdn-types/src/net.rs b/proxmox-sdn-types/src/net.rs
index 3e523fb12d9b..21822bacab1c 100644
--- a/proxmox-sdn-types/src/net.rs
+++ b/proxmox-sdn-types/src/net.rs
@@ -139,7 +139,7 @@ impl Default for NetSelector {
/// between fabrics on the same node. It contains the [`NetSystemId`] and the [`NetSelector`].
/// e.g.: "1921.6800.1002.00"
#[api]
-#[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Net {
afi: NetAFI,
area: NetArea,
@@ -147,6 +147,8 @@ pub struct Net {
selector: NetSelector,
}
+proxmox_serde::forward_serialize_to_display!(Net);
+proxmox_serde::forward_deserialize_to_from_str!(Net);
impl UpdaterType for Net {
type Updater = Option<Net>;
diff --git a/proxmox-ve-config/src/common/valid.rs b/proxmox-ve-config/src/common/valid.rs
index 1f92ef9bb409..f59ddd0a8806 100644
--- a/proxmox-ve-config/src/common/valid.rs
+++ b/proxmox-ve-config/src/common/valid.rs
@@ -1,5 +1,7 @@
use std::ops::Deref;
+use serde::{Deserialize, Serialize};
+
/// A wrapper type for validatable structs.
///
/// It can only be constructed by implementing the [`Validatable`] type for a struct. Its contents
@@ -8,7 +10,7 @@ use std::ops::Deref;
///
/// If you want to edit the content, this struct has to be unwrapped via [`Valid<T>::into_inner`].
#[repr(transparent)]
-#[derive(Clone, Default, Debug)]
+#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Valid<T>(T);
impl<T> Valid<T> {
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index 10025b3544b9..f2b7c7290c08 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -1,12 +1,20 @@
use std::net::{IpAddr, Ipv4Addr};
+
use tracing;
-use proxmox_frr::ser::{self};
+use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
+use proxmox_frr::ser::ospf::{self, OspfInterface, OspfRouter};
+use proxmox_frr::ser::route_map::{
+ AccessAction, AccessListName, AccessListOrPrefixList, RouteMapEntry, RouteMapMatch,
+ RouteMapMatchInner, RouteMapName, RouteMapSet,
+};
+use proxmox_frr::ser::{
+ self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap,
+};
use proxmox_network_types::ip_address::Cidr;
use proxmox_sdn_types::net::Net;
use crate::common::valid::Valid;
-
use crate::sdn::fabric::section_config::protocol::{
openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties},
ospf::OspfInterfaceProperties,
@@ -17,11 +25,11 @@ use crate::sdn::fabric::{FabricConfig, FabricEntry};
/// Constructs the FRR config from the the passed [`Valid<FabricConfig>`].
///
/// Iterates over the [`FabricConfig`] and constructs all the FRR routers, interfaces, route-maps,
-/// etc. which area all appended to the passed [`FrrConfig`].
+/// etc.
pub fn build_fabric(
current_node: NodeId,
config: Valid<FabricConfig>,
- frr_config: &mut ser::FrrConfig,
+ frr_config: &mut FrrConfig,
) -> Result<(), anyhow::Error> {
let mut routemap_seq = 100;
let mut current_router_id: Option<Ipv4Addr> = None;
@@ -48,7 +56,15 @@ pub fn build_fabric(
.as_ref()
.ok_or_else(|| anyhow::anyhow!("no IPv4 or IPv6 set for node"))?;
let (router_name, router_item) = build_openfabric_router(fabric_id, net.clone())?;
- frr_config.router.insert(router_name, router_item);
+
+ if frr_config
+ .openfabric
+ .router
+ .insert(router_name, router_item)
+ .is_some()
+ {
+ tracing::error!("duplicate OpenFabric router");
+ }
// Create dummy interface for fabric
let (interface, interface_name) = build_openfabric_dummy_interface(
@@ -58,6 +74,7 @@ pub fn build_fabric(
)?;
if frr_config
+ .openfabric
.interfaces
.insert(interface_name, interface)
.is_some()
@@ -79,6 +96,7 @@ pub fn build_fabric(
)?;
if frr_config
+ .openfabric
.interfaces
.insert(interface_name, interface)
.is_some()
@@ -91,70 +109,85 @@ pub fn build_fabric(
let rule = ser::route_map::AccessListRule {
action: ser::route_map::AccessAction::Permit,
network: Cidr::from(ipv4cidr),
+ is_ipv6: false,
seq: None,
};
- let access_list_name = ser::route_map::AccessListName::new(format!(
- "pve_openfabric_{}_ips",
- fabric_id
- ));
- frr_config.access_lists.push(ser::route_map::AccessList {
- name: access_list_name,
- rules: vec![rule],
- });
+ let access_list_name =
+ AccessListName::new(format!("pve_openfabric_{}_ips", fabric_id));
+ frr_config.access_lists.insert(access_list_name, vec![rule]);
}
if let Some(ipv6cidr) = fabric.ip6_prefix() {
let rule = ser::route_map::AccessListRule {
action: ser::route_map::AccessAction::Permit,
network: Cidr::from(ipv6cidr),
+ is_ipv6: true,
seq: None,
};
- let access_list_name = ser::route_map::AccessListName::new(format!(
- "pve_openfabric_{}_ip6s",
- fabric_id
- ));
- frr_config.access_lists.push(ser::route_map::AccessList {
- name: access_list_name,
- rules: vec![rule],
- });
+ let access_list_name =
+ AccessListName::new(format!("pve_openfabric_{}_ip6s", fabric_id));
+ frr_config.access_lists.insert(access_list_name, vec![rule]);
}
if let Some(ipv4) = node.ip() {
// create route-map
- frr_config.routemaps.push(build_openfabric_routemap(
- fabric_id,
- IpAddr::V4(ipv4),
- routemap_seq,
- ));
- routemap_seq += 10;
+ let (routemap_name, routemap_rule) =
+ build_openfabric_routemap(fabric_id, IpAddr::V4(ipv4), routemap_seq);
+
+ if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+ routemap.push(routemap_rule)
+ } else {
+ frr_config
+ .routemaps
+ .insert(routemap_name.clone(), vec![routemap_rule]);
+ }
- let protocol_routemap = ser::route_map::ProtocolRouteMap {
- is_ipv6: false,
- protocol: ser::route_map::ProtocolType::Openfabric,
- routemap_name: ser::route_map::RouteMapName::new(
- "pve_openfabric".to_owned(),
- ),
- };
+ routemap_seq += 10;
- frr_config.protocol_routemaps.insert(protocol_routemap);
+ if let Some(routemap) = frr_config
+ .protocol_routemaps
+ .get_mut(&FrrProtocol::Openfabric)
+ {
+ routemap.v4 = Some(routemap_name);
+ } else {
+ frr_config.protocol_routemaps.insert(
+ FrrProtocol::Openfabric,
+ IpProtocolRouteMap {
+ v4: Some(routemap_name),
+ v6: None,
+ },
+ );
+ }
}
+
if let Some(ipv6) = node.ip6() {
// create route-map
- frr_config.routemaps.push(build_openfabric_routemap(
- fabric_id,
- IpAddr::V6(ipv6),
- routemap_seq,
- ));
- routemap_seq += 10;
+ let (routemap_name, routemap_rule) =
+ build_openfabric_routemap(fabric_id, IpAddr::V6(ipv6), routemap_seq);
+
+ if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+ routemap.push(routemap_rule)
+ } else {
+ frr_config
+ .routemaps
+ .insert(routemap_name.clone(), vec![routemap_rule]);
+ }
- let protocol_routemap = ser::route_map::ProtocolRouteMap {
- is_ipv6: true,
- protocol: ser::route_map::ProtocolType::Openfabric,
- routemap_name: ser::route_map::RouteMapName::new(
- "pve_openfabric6".to_owned(),
- ),
- };
+ routemap_seq += 10;
- frr_config.protocol_routemaps.insert(protocol_routemap);
+ if let Some(routemap) = frr_config
+ .protocol_routemaps
+ .get_mut(&FrrProtocol::Openfabric)
+ {
+ routemap.v6 = Some(routemap_name);
+ } else {
+ frr_config.protocol_routemaps.insert(
+ FrrProtocol::Openfabric,
+ IpProtocolRouteMap {
+ v4: None,
+ v6: Some(routemap_name),
+ },
+ );
+ }
}
}
FabricEntry::Ospf(ospf_entry) => {
@@ -169,14 +202,17 @@ pub fn build_fabric(
let frr_word_area = ser::FrrWord::new(fabric.properties().area.to_string())?;
let frr_area = ser::ospf::Area::new(frr_word_area)?;
- let (router_name, router_item) = build_ospf_router(*router_id)?;
- frr_config.router.insert(router_name, router_item);
+
+ if frr_config.ospf.router.is_none() {
+ frr_config.ospf.router = Some(build_ospf_router(*router_id)?);
+ }
// Add dummy interface
let (interface, interface_name) =
build_ospf_dummy_interface(fabric_id, frr_area.clone())?;
if frr_config
+ .ospf
.interfaces
.insert(interface_name, interface)
.is_some()
@@ -191,11 +227,12 @@ pub fn build_fabric(
build_ospf_interface(frr_area.clone(), interface)?;
if frr_config
+ .ospf
.interfaces
.insert(interface_name, interface)
.is_some()
{
- tracing::warn!("An interface cannot be in multiple openfabric fabrics");
+ tracing::warn!("An interface cannot be in multiple ospf fabrics");
}
}
@@ -207,53 +244,59 @@ pub fn build_fabric(
network: Cidr::from(
fabric.ip_prefix().expect("fabric must have a ipv4 prefix"),
),
+ is_ipv6: false,
seq: None,
};
- frr_config.access_lists.push(ser::route_map::AccessList {
- name: access_list_name,
- rules: vec![rule],
- });
+ frr_config.access_lists.insert(access_list_name, vec![rule]);
- let routemap = build_ospf_dummy_routemap(
+ let (routemap_name, routemap_rule) = build_ospf_dummy_routemap(
fabric_id,
node.ip().expect("node must have an ipv4 address"),
routemap_seq,
)?;
routemap_seq += 10;
- frr_config.routemaps.push(routemap);
- let protocol_routemap = ser::route_map::ProtocolRouteMap {
- is_ipv6: false,
- protocol: ser::route_map::ProtocolType::Ospf,
- routemap_name: ser::route_map::RouteMapName::new("pve_ospf".to_owned()),
- };
+ if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+ routemap.push(routemap_rule)
+ } else {
+ frr_config
+ .routemaps
+ .insert(routemap_name.clone(), vec![routemap_rule]);
+ }
- frr_config.protocol_routemaps.insert(protocol_routemap);
+ if let Some(routemap) = frr_config.protocol_routemaps.get_mut(&FrrProtocol::Ospf) {
+ routemap.v4 = Some(routemap_name);
+ } else {
+ frr_config.protocol_routemaps.insert(
+ FrrProtocol::Ospf,
+ IpProtocolRouteMap {
+ v4: Some(routemap_name),
+ v6: None,
+ },
+ );
+ }
}
}
}
+
Ok(())
}
/// Helper that builds a OSPF router with a the router_id.
-fn build_ospf_router(router_id: Ipv4Addr) -> Result<(ser::RouterName, ser::Router), anyhow::Error> {
- let ospf_router = ser::ospf::OspfRouter { router_id };
- let router_item = ser::Router::Ospf(ospf_router);
- let router_name = ser::RouterName::Ospf(ser::ospf::OspfRouterName);
- Ok((router_name, router_item))
+fn build_ospf_router(router_id: Ipv4Addr) -> Result<OspfRouter, anyhow::Error> {
+ Ok(ser::ospf::OspfRouter { router_id })
}
/// Helper that builds a OpenFabric router from a fabric_id and a [`Net`].
fn build_openfabric_router(
fabric_id: &FabricId,
net: Net,
-) -> Result<(ser::RouterName, ser::Router), anyhow::Error> {
- let ofr = ser::openfabric::OpenfabricRouter { net };
- let router_item = ser::Router::Openfabric(ofr);
- let frr_word_id = ser::FrrWord::new(fabric_id.to_string())?;
- let router_name = ser::RouterName::Openfabric(frr_word_id.into());
+) -> Result<(OpenfabricRouterName, OpenfabricRouter), anyhow::Error> {
+ let router_item = ser::openfabric::OpenfabricRouter { net };
+ let frr_word_id = FrrWord::new(fabric_id.to_string())?;
+ let router_name = frr_word_id.into();
Ok((router_name, router_item))
}
@@ -261,10 +304,10 @@ fn build_openfabric_router(
fn build_ospf_interface(
area: ser::ospf::Area,
interface: &OspfInterfaceProperties,
-) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
+) -> Result<(Interface<OspfInterface>, InterfaceName), anyhow::Error> {
let frr_interface = ser::ospf::OspfInterface {
area,
- // Interfaces are always none-passive
+ // Interfaces are always non-passive
passive: None,
network_type: if interface.ip.is_some() {
None
@@ -273,21 +316,21 @@ fn build_ospf_interface(
},
};
- let interface_name = ser::InterfaceName::Ospf(interface.name.as_str().try_into()?);
+ let interface_name = interface.name.as_ref().try_into()?;
Ok((frr_interface.into(), interface_name))
}
/// Helper that builds the OSPF dummy interface using the [`FabricId`] and the [`ospf::Area`].
fn build_ospf_dummy_interface(
fabric_id: &FabricId,
- area: ser::ospf::Area,
-) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
+ area: ospf::Area,
+) -> Result<(Interface<OspfInterface>, InterfaceName), anyhow::Error> {
let frr_interface = ser::ospf::OspfInterface {
area,
passive: Some(true),
network_type: None,
};
- let interface_name = ser::InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
+ let interface_name = format!("dummy_{}", fabric_id).try_into()?;
Ok((frr_interface.into(), interface_name))
}
@@ -301,8 +344,8 @@ fn build_openfabric_interface(
fabric_config: &OpenfabricProperties,
is_ipv4: bool,
is_ipv6: bool,
-) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
- let frr_word = ser::FrrWord::new(fabric_id.to_string())?;
+) -> Result<(Interface<OpenfabricInterface>, InterfaceName), anyhow::Error> {
+ let frr_word = FrrWord::new(fabric_id.to_string())?;
let mut frr_interface = ser::openfabric::OpenfabricInterface {
fabric_id: frr_word.into(),
// Every interface is not passive by default
@@ -319,7 +362,7 @@ fn build_openfabric_interface(
if frr_interface.hello_interval.is_none() {
frr_interface.hello_interval = fabric_config.hello_interval;
}
- let interface_name = ser::InterfaceName::Openfabric(interface.name.as_str().try_into()?);
+ let interface_name = interface.name.as_str().try_into()?;
Ok((frr_interface.into(), interface_name))
}
@@ -328,18 +371,18 @@ fn build_openfabric_dummy_interface(
fabric_id: &FabricId,
is_ipv4: bool,
is_ipv6: bool,
-) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
- let frr_word = ser::FrrWord::new(fabric_id.to_string())?;
+) -> Result<(Interface<OpenfabricInterface>, InterfaceName), anyhow::Error> {
+ let frr_word = FrrWord::new(fabric_id.to_string())?;
let frr_interface = ser::openfabric::OpenfabricInterface {
fabric_id: frr_word.into(),
- hello_interval: None,
passive: Some(true),
- csnp_interval: None,
- hello_multiplier: None,
is_ipv4,
is_ipv6,
+ hello_interval: None,
+ csnp_interval: None,
+ hello_multiplier: None,
};
- let interface_name = ser::InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
+ let interface_name = format!("dummy_{}", fabric_id).try_into()?;
Ok((frr_interface.into(), interface_name))
}
@@ -348,29 +391,32 @@ fn build_openfabric_routemap(
fabric_id: &FabricId,
router_ip: IpAddr,
seq: u32,
-) -> ser::route_map::RouteMap {
+) -> (RouteMapName, RouteMapEntry) {
let routemap_name = match router_ip {
IpAddr::V4(_) => ser::route_map::RouteMapName::new("pve_openfabric".to_owned()),
IpAddr::V6(_) => ser::route_map::RouteMapName::new("pve_openfabric6".to_owned()),
};
- ser::route_map::RouteMap {
- name: routemap_name.clone(),
- seq,
- action: ser::route_map::AccessAction::Permit,
- matches: vec![match router_ip {
- IpAddr::V4(_) => {
- ser::route_map::RouteMapMatch::V4(ser::route_map::RouteMapMatchInner::IpAddress(
- ser::route_map::AccessListName::new(format!("pve_openfabric_{fabric_id}_ips")),
- ))
- }
- IpAddr::V6(_) => {
- ser::route_map::RouteMapMatch::V6(ser::route_map::RouteMapMatchInner::IpAddress(
- ser::route_map::AccessListName::new(format!("pve_openfabric_{fabric_id}_ip6s")),
- ))
- }
- }],
- sets: vec![ser::route_map::RouteMapSet::IpSrc(router_ip)],
- }
+ (
+ routemap_name,
+ RouteMapEntry {
+ seq,
+ action: ser::route_map::AccessAction::Permit,
+ matches: vec![match router_ip {
+ IpAddr::V4(_) => RouteMapMatch::V4(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::AccessList(AccessListName::new(format!(
+ "pve_openfabric_{fabric_id}_ips"
+ ))),
+ )),
+ IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::AccessList(AccessListName::new(format!(
+ "pve_openfabric_{fabric_id}_ip6s"
+ ))),
+ )),
+ }],
+ sets: vec![RouteMapSet::Src(router_ip)],
+ custom_frr_config: Vec::new(),
+ },
+ )
}
/// Helper that builds a RouteMap for the OSPF protocol.
@@ -378,20 +424,20 @@ fn build_ospf_dummy_routemap(
fabric_id: &FabricId,
router_ip: Ipv4Addr,
seq: u32,
-) -> Result<ser::route_map::RouteMap, anyhow::Error> {
+) -> Result<(RouteMapName, RouteMapEntry), anyhow::Error> {
let routemap_name = ser::route_map::RouteMapName::new("pve_ospf".to_owned());
// create route-map
- let routemap = ser::route_map::RouteMap {
- name: routemap_name.clone(),
+ let routemap = RouteMapEntry {
seq,
- action: ser::route_map::AccessAction::Permit,
- matches: vec![ser::route_map::RouteMapMatch::V4(
- ser::route_map::RouteMapMatchInner::IpAddress(ser::route_map::AccessListName::new(
- format!("pve_ospf_{fabric_id}_ips"),
- )),
- )],
- sets: vec![ser::route_map::RouteMapSet::IpSrc(IpAddr::from(router_ip))],
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::AccessList(AccessListName::new(format!(
+ "pve_ospf_{fabric_id}_ips"
+ ))),
+ ))],
+ sets: vec![RouteMapSet::Src(IpAddr::from(router_ip))],
+ custom_frr_config: Vec::new(),
};
- Ok(routemap)
+ Ok((routemap_name, routemap))
}
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap
index 98eb50415e36..052a3f7e501c 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router openfabric uwu
net 49.0001.1921.6800.2008.00
exit
@@ -31,4 +32,3 @@ route-map pve_openfabric permit 100
exit
!
ip protocol openfabric route-map pve_openfabric
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap
index 4453ac49377f..f456e819098a 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router openfabric uwu
net 49.0001.1921.6800.2009.00
exit
@@ -30,4 +31,3 @@ route-map pve_openfabric permit 100
exit
!
ip protocol openfabric route-map pve_openfabric
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap
index 48ac9092045e..ae81da3a4a00 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap
@@ -1,35 +1,35 @@
---
source: proxmox-ve-config/tests/fabric/main.rs
expression: output
-snapshot_kind: text
---
+!
router openfabric uwu
net 49.0001.1921.6800.2008.00
exit
!
interface dummy_uwu
- ipv6 router openfabric uwu
ip router openfabric uwu
+ ipv6 router openfabric uwu
openfabric passive
exit
!
interface ens19
- ipv6 router openfabric uwu
ip router openfabric uwu
+ ipv6 router openfabric uwu
openfabric hello-interval 4
exit
!
interface ens20
- ipv6 router openfabric uwu
ip router openfabric uwu
+ ipv6 router openfabric uwu
openfabric hello-interval 4
openfabric hello-multiplier 50
exit
!
-access-list pve_openfabric_uwu_ips permit 192.168.2.0/24
-!
ipv6 access-list pve_openfabric_uwu_ip6s permit 2001:db8::/64
!
+access-list pve_openfabric_uwu_ips permit 192.168.2.0/24
+!
route-map pve_openfabric permit 100
match ip address pve_openfabric_uwu_ips
set src 192.168.2.8
@@ -43,4 +43,3 @@ exit
ip protocol openfabric route-map pve_openfabric
!
ipv6 protocol openfabric route-map pve_openfabric6
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap
index d7ab1d7e2a61..21c75e4f5861 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap
@@ -1,8 +1,8 @@
---
source: proxmox-ve-config/tests/fabric/main.rs
expression: output
-snapshot_kind: text
---
+!
router openfabric uwu
net 49.0001.0000.0000.000a.00
exit
@@ -30,5 +30,5 @@ route-map pve_openfabric6 permit 100
set src a:b::a
exit
!
-ipv6 protocol openfabric route-map pve_openfabric6
!
+ipv6 protocol openfabric route-map pve_openfabric6
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap
index ad6c6db8eb8b..5e0c18eb2493 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router openfabric test1
net 49.0001.1921.6800.2009.00
exit
@@ -46,4 +47,3 @@ route-map pve_openfabric permit 110
exit
!
ip protocol openfabric route-map pve_openfabric
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap
index a303f31f3d1a..ee47866edd67 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router ospf
ospf router-id 10.10.10.1
exit
@@ -29,4 +30,3 @@ route-map pve_ospf permit 100
exit
!
ip protocol ospf route-map pve_ospf
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap
index 46c30b22abdf..209f3406757b 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router ospf
ospf router-id 10.10.10.2
exit
@@ -25,4 +26,3 @@ route-map pve_ospf permit 100
exit
!
ip protocol ospf route-map pve_ospf
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap
index 1d2a7c3c272d..225d60bf7edb 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router ospf
ospf router-id 192.168.1.9
exit
@@ -42,4 +43,3 @@ route-map pve_ospf permit 110
exit
!
ip protocol ospf route-map pve_ospf
-!
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH proxmox-ve-rs v2 6/8] frr: add isis configuration and templates
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (4 preceding siblings ...)
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 5/8] frr: add template serializer and serialize fabrics using templates Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 7/8] frr: support custom frr configuration lines Gabriel Goller
` (12 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
Add templates and rust configuration types to render configuration for
the isis daemon. This allows us to generate the config in perl, then
pass it to rust and then render the template in rust.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
.../templates/frr.conf.jinja | 1 +
proxmox-frr-templates/templates/isisd.jinja | 32 +++++++++++++
proxmox-frr/src/ser/isis.rs | 47 +++++++++++++++++++
proxmox-frr/src/ser/mod.rs | 12 +++++
proxmox-frr/src/ser/serializer.rs | 1 +
5 files changed, 93 insertions(+)
create mode 100644 proxmox-frr-templates/templates/isisd.jinja
create mode 100644 proxmox-frr/src/ser/isis.rs
diff --git a/proxmox-frr-templates/templates/frr.conf.jinja b/proxmox-frr-templates/templates/frr.conf.jinja
index 8458c6a61112..f8802f845332 100644
--- a/proxmox-frr-templates/templates/frr.conf.jinja
+++ b/proxmox-frr-templates/templates/frr.conf.jinja
@@ -1,3 +1,4 @@
+{% include "isisd.jinja" %}
{% include "fabricd.jinja" %}
{% include "ospfd.jinja" %}
{% include "access_lists.jinja" %}
diff --git a/proxmox-frr-templates/templates/isisd.jinja b/proxmox-frr-templates/templates/isisd.jinja
new file mode 100644
index 000000000000..75f066166ad2
--- /dev/null
+++ b/proxmox-frr-templates/templates/isisd.jinja
@@ -0,0 +1,32 @@
+{% from "interface.jinja" import interface %}
+{% for router_name, router_config in isis.router|items %}
+!
+router isis {{ router_name }}
+ net {{ router_config.net }}
+{% if router_config.redistribute.ipv4_connected %}
+ redistribute ipv4 connected {{ router_config.redistribute.ipv4_connected }}
+{% endif %}
+{% if router_config.redistribute.ipv6_connected %}
+ redistribute ipv6 connected {{ router_config.redistribute.ipv6_connected }}
+{% endif %}
+{% if router_config.log_adjacency_changes %}
+ log-adjacency-changes
+{% endif %}
+{% for line in router_config.custom_frr_config %}
+{{ line }}
+{% endfor %}
+exit
+{% endfor %}
+{% for interface_name, interface_config in isis.interfaces|items %}
+{% call interface(interface_name, interface_config.addresses) %}
+{% if interface_config.domain and interface_config.is_ipv4 %}
+ ip router isis {{ interface_config.domain }}
+{% endif %}
+{% if interface_config.domain and interface_config.is_ipv6 %}
+ ipv6 router isis {{ interface_config.domain }}
+{% endif %}
+{% for line in interface_config.custom_frr_config %}
+{{ line }}
+{% endfor %}
+{% endcall %}
+{% endfor %}
diff --git a/proxmox-frr/src/ser/isis.rs b/proxmox-frr/src/ser/isis.rs
new file mode 100644
index 000000000000..211c5b21e9e1
--- /dev/null
+++ b/proxmox-frr/src/ser/isis.rs
@@ -0,0 +1,47 @@
+use std::fmt::Debug;
+
+use proxmox_sdn_types::net::Net;
+use serde::Deserialize;
+use serde::Serialize;
+
+use crate::ser::FrrWord;
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct IsisRouterName(FrrWord);
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub enum IsisLevel {
+ #[serde(rename = "level-1")]
+ Level1,
+ #[serde(rename = "level-2")]
+ Level2,
+ #[serde(rename = "level-1-2")]
+ Level12,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct Redistribute {
+ ipv4_connected: IsisLevel,
+ ipv6_connected: IsisLevel,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct IsisRouter {
+ pub net: Net,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub log_adjacency_changes: Option<bool>,
+ pub redistribute: Option<Redistribute>,
+ #[serde(default)]
+ pub custom_frr_config: Vec<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct IsisInterface {
+ pub domain: IsisRouterName,
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub is_ipv4: bool,
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub is_ipv6: bool,
+ #[serde(default)]
+ pub custom_frr_config: Vec<String>,
+}
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
index 86813c7d6415..55bbe0522cf5 100644
--- a/proxmox-frr/src/ser/mod.rs
+++ b/proxmox-frr/src/ser/mod.rs
@@ -1,3 +1,4 @@
+pub mod isis;
pub mod openfabric;
pub mod ospf;
pub mod route_map;
@@ -178,6 +179,9 @@ pub struct FrrConfig {
pub openfabric: OpenfabricFrrConfig,
#[serde(default)]
pub ospf: OspfFrrConfig,
+ #[serde(default)]
+ pub isis: IsisFrrConfig,
+
#[serde(default)]
pub protocol_routemaps: BTreeMap<FrrProtocol, IpProtocolRouteMap>,
#[serde(default)]
@@ -194,6 +198,14 @@ pub struct OpenfabricFrrConfig {
pub interfaces: BTreeMap<InterfaceName, Interface<openfabric::OpenfabricInterface>>,
}
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub struct IsisFrrConfig {
+ #[serde(default)]
+ pub router: BTreeMap<isis::IsisRouterName, isis::IsisRouter>,
+ #[serde(default)]
+ pub interfaces: BTreeMap<InterfaceName, Interface<isis::IsisInterface>>,
+}
+
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct OspfFrrConfig {
#[serde(default)]
diff --git a/proxmox-frr/src/ser/serializer.rs b/proxmox-frr/src/ser/serializer.rs
index 51c46729c8ce..1f5f4897fea3 100644
--- a/proxmox-frr/src/ser/serializer.rs
+++ b/proxmox-frr/src/ser/serializer.rs
@@ -5,6 +5,7 @@ use crate::ser::FrrConfig;
pub static TEMPLATES: phf::Map<&'static str, &'static str> = phf::phf_map! {
"fabricd.jinja" => include_str!("/usr/share/proxmox-frr/templates/fabricd.jinja"),
+ "isisd.jinja" => include_str!("/usr/share/proxmox-frr/templates/isisd.jinja"),
"ospfd.jinja" => include_str!("/usr/share/proxmox-frr/templates/ospfd.jinja"),
"interface.jinja" => include_str!("/usr/share/proxmox-frr/templates/interface.jinja"),
"access_lists.jinja" => include_str!("/usr/share/proxmox-frr/templates/access_lists.jinja"),
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH proxmox-ve-rs v2 7/8] frr: support custom frr configuration lines
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (5 preceding siblings ...)
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 6/8] frr: add isis configuration and templates Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 8/8] frr: add bgp support with templates and serialization Gabriel Goller
` (11 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
When merging the frr.conf.local with the frr.conf, some lines cannot be
merged and we need to add custom frr config lines to the rust
configuration. Add the vec of lines and just dump them into the
template.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr-templates/templates/frr.conf.jinja | 3 +++
proxmox-frr/src/ser/mod.rs | 2 ++
2 files changed, 5 insertions(+)
diff --git a/proxmox-frr-templates/templates/frr.conf.jinja b/proxmox-frr-templates/templates/frr.conf.jinja
index f8802f845332..68c159199f4e 100644
--- a/proxmox-frr-templates/templates/frr.conf.jinja
+++ b/proxmox-frr-templates/templates/frr.conf.jinja
@@ -1,4 +1,7 @@
{% include "isisd.jinja" %}
+{% for line in custom_frr_config %}
+{{ line }}
+{% endfor %}
{% include "fabricd.jinja" %}
{% include "ospfd.jinja" %}
{% include "access_lists.jinja" %}
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
index 55bbe0522cf5..44a54586c7c5 100644
--- a/proxmox-frr/src/ser/mod.rs
+++ b/proxmox-frr/src/ser/mod.rs
@@ -188,6 +188,8 @@ pub struct FrrConfig {
pub routemaps: BTreeMap<RouteMapName, Vec<RouteMapEntry>>,
#[serde(default)]
pub access_lists: BTreeMap<AccessListName, Vec<AccessListRule>>,
+ #[serde(default)]
+ pub custom_frr_config: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH proxmox-ve-rs v2 8/8] frr: add bgp support with templates and serialization
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (6 preceding siblings ...)
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 7/8] frr: support custom frr configuration lines Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH proxmox-perl-rs v2 1/1] sdn: add function to generate the frr config for all daemons Gabriel Goller
` (10 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
Implements bgp routing configuration (rust types and templates)
including routers, neighbor groups, address families (IPv4/IPv6 unicast,
L2VPN EVPN), VRFs, route redistribution, and prefix lists.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
.../templates/bgp_router.jinja | 128 ++++++++++++
proxmox-frr-templates/templates/bgpd.jinja | 35 ++++
.../templates/frr.conf.jinja | 3 +
.../templates/ip_routes.jinja | 8 +
.../templates/prefix_lists.jinja | 6 +
proxmox-frr/src/ser/bgp.rs | 185 ++++++++++++++++++
proxmox-frr/src/ser/mod.rs | 31 ++-
proxmox-frr/src/ser/serializer.rs | 4 +
8 files changed, 399 insertions(+), 1 deletion(-)
create mode 100644 proxmox-frr-templates/templates/bgp_router.jinja
create mode 100644 proxmox-frr-templates/templates/bgpd.jinja
create mode 100644 proxmox-frr-templates/templates/ip_routes.jinja
create mode 100644 proxmox-frr-templates/templates/prefix_lists.jinja
create mode 100644 proxmox-frr/src/ser/bgp.rs
diff --git a/proxmox-frr-templates/templates/bgp_router.jinja b/proxmox-frr-templates/templates/bgp_router.jinja
new file mode 100644
index 000000000000..e90ee21c64cb
--- /dev/null
+++ b/proxmox-frr-templates/templates/bgp_router.jinja
@@ -0,0 +1,128 @@
+{% macro address_family_common(common_address_family) -%}
+{% for vrf in common_address_family.import_vrf %}
+ import vrf {{ vrf }}
+{% endfor %}
+{% for neighbor in common_address_family.neighbors %}
+ neighbor {{ neighbor.name }} activate
+ {% if neighbor.soft_reconfiguration_inbound %}
+ neighbor {{ neighbor.name }} soft-reconfiguration inbound
+ {% endif %}
+ {% if neighbor.route_map_in %}
+ neighbor {{ neighbor.name }} route-map {{ neighbor.route_map_in }} in
+ {% endif %}
+ {% if neighbor.route_map_out %}
+ neighbor {{ neighbor.name }} route-map {{ neighbor.route_map_out }} out
+ {% endif %}
+{% endfor -%}
+{% endmacro -%}
+{% macro bgp_router(router_config) %}
+ bgp router-id {{ router_config.router_id }}
+{% if router_config.hard_administrative_reset == false %}
+ no bgp hard-administrative-reset
+{% endif %}
+{% if router_config.default_ipv4_unicast == false %}
+ no bgp default ipv4-unicast
+{% endif %}
+{% if router_config.coalesce_time %}
+ coalesce-time {{ router_config.coalesce_time }}
+{% endif %}
+{% if router_config.graceful_restart_notification == false %}
+ no bgp graceful-restart notification
+{% endif %}
+{% if router_config.disable_ebgp_connected_route_check %}
+ bgp disable-ebgp-connected-route-check
+{% endif %}
+{% if router_config.bestpath_as_path_multipath_relax %}
+ bgp bestpath as-path multipath-relax
+{% endif %}
+{% for neighbor_group in router_config.neighbor_groups %}
+ neighbor {{ neighbor_group.name }} peer-group
+ neighbor {{ neighbor_group.name }} remote-as {{ neighbor_group.remote_as }}
+{% if neighbor_group.bfd %}
+ neighbor {{ neighbor_group.name }} bfd
+{% endif %}
+{% if neighbor_group.ebgp_multihop %}
+ neighbor {{ neighbor_group.name }} ebgp-multihop {{ neighbor_group.ebgp_multihop }}
+{% endif %}
+{% if neighbor_group.update_source %}
+ neighbor {{ neighbor_group.name }} update-source {{ neighbor_group.update_source }}
+{% endif %}
+{% for ip in neighbor_group.ips %}
+ neighbor {{ ip }} peer-group {{ neighbor_group.name }}
+{% endfor %}
+{% for interface in neighbor_group.interfaces %}
+ neighbor {{ interface }} interface peer-group {{ neighbor_group.name }}
+{% endfor %}
+{% endfor %}
+{% for line in router_config.custom_frr_config %}
+{{ line }}
+{% endfor %}
+{% if router_config.address_families.ipv4_unicast %}
+ !
+ address-family ipv4 unicast
+{% for network in router_config.address_families.ipv4_unicast.networks %}
+ network {{ network }}
+{% endfor %}
+{{ address_family_common(router_config.address_families.ipv4_unicast) -}}
+{% for redistribute in router_config.address_families.ipv4_unicast.redistribute %}
+ redistribute {{ redistribute.protocol }}{{ (" metric " ~ redistribute.metric) if redistribute.metric }}{{ (" route-map " ~ redistribute.route_map) if redistribute.route_map }}
+{% endfor %}
+{% for line in router_config.address_families.ipv4_unicast.custom_frr_config %}
+{{ line }}
+{% endfor %}
+ exit-address-family
+{% endif %}
+{% if router_config.address_families.ipv6_unicast %}
+ !
+ address-family ipv6 unicast
+{% for network in router_config.address_families.ipv6_unicast.networks %}
+ network {{ network }}
+{% endfor %}
+{{ address_family_common(router_config.address_families.ipv6_unicast) -}}
+{% for redistribute in router_config.address_families.ipv6_unicast.redistribute %}
+ redistribute {{ redistribute.protocol }}{{ (" metric " ~ redistribute.metric) if redistribute.metric }}{{ (" route-map " ~ redistribute.route_map) if redistribute.route_map }}
+{% endfor %}
+{% for line in router_config.address_families.ipv6_unicast.custom_frr_config %}
+{{ line }}
+{% endfor %}
+ exit-address-family
+{% endif %}
+{% if router_config.address_families.l2vpn_evpn %}
+ !
+ address-family l2vpn evpn
+{{ address_family_common(router_config.address_families.l2vpn_evpn) -}}
+{% if router_config.address_families.l2vpn_evpn.advertise_all_vni %}
+ advertise-all-vni
+{% endif %}
+{% if router_config.address_families.l2vpn_evpn.advertise_default_gw %}
+ advertise-default-gw
+{% endif %}
+{% if router_config.address_families.l2vpn_evpn.autort_as %}
+ autort as {{ router_config.address_families.l2vpn_evpn.autort_as }}
+{% endif %}
+{% for default_originate in router_config.address_families.l2vpn_evpn.default_originate %}
+ default-originate {{ default_originate }}
+{% endfor %}
+{% if router_config.address_families.l2vpn_evpn.advertise_ipv4_unicast %}
+ advertise ipv4 unicast
+{% endif %}
+{% if router_config.address_families.l2vpn_evpn.advertise_ipv6_unicast %}
+ advertise ipv6 unicast
+{% endif %}
+{% if router_config.address_families.l2vpn_evpn.route_targets %}
+{% for import in router_config.address_families.l2vpn_evpn.route_targets.import %}
+ route-target import {{ import }}
+{% endfor %}
+{% for export in router_config.address_families.l2vpn_evpn.route_targets.export %}
+ route-target export {{ export }}
+{% endfor %}
+{% for both in router_config.address_families.l2vpn_evpn.route_targets.both %}
+ route-target both {{ both }}
+{% endfor %}
+{% endif %}
+{% for line in router_config.address_families.l2vpn_evpn.custom_frr_config %}
+{{ line }}
+{% endfor %}
+ exit-address-family
+{% endif %}
+{% endmacro -%}
diff --git a/proxmox-frr-templates/templates/bgpd.jinja b/proxmox-frr-templates/templates/bgpd.jinja
new file mode 100644
index 000000000000..cdd0a50abf8c
--- /dev/null
+++ b/proxmox-frr-templates/templates/bgpd.jinja
@@ -0,0 +1,35 @@
+{% from "bgp_router.jinja" import bgp_router %}
+{% for vrf_name, vrf in bgp.vrfs|items %}
+!
+vrf {{ vrf_name }}
+{% if vrf.vni %}
+ vni {{ vrf.vni }}
+{% for ip_route in vrf.ip_routes %}
+{% if ip_route.vrf %}
+ {{ "ipv6" if ip_route.is_ipv6 else "ip" }} route {{ ip_route.prefix }} {{ ip_route.via }} {{ ip_route.vrf }}
+{% else %}
+ {{ "ipv6" if ip_route.is_ipv6 else "ip" }} route {{ ip_route.prefix }} {{ ip_route.via }}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% for line in vrf.custom_frr_config %}
+{{ line }}
+{% endfor %}
+exit-vrf
+{% endfor %}
+{% for vrf_name, router_config in bgp.vrf_router|items %}
+!
+{% if vrf_name == "default" %}
+router bgp {{ router_config.asn }}
+{% else %}
+router bgp {{ router_config.asn }} vrf {{ vrf_name }}
+{% endif %}
+{{ bgp_router(router_config) -}}
+exit
+{% endfor %}
+{% for view_id, router_config in bgp.view_router|items %}
+!
+router bgp {{ router_config.asn }} view {{ view_id }}
+{{ bgp_router(router_config) -}}
+exit
+{% endfor %}
diff --git a/proxmox-frr-templates/templates/frr.conf.jinja b/proxmox-frr-templates/templates/frr.conf.jinja
index 68c159199f4e..1f98489e09fb 100644
--- a/proxmox-frr-templates/templates/frr.conf.jinja
+++ b/proxmox-frr-templates/templates/frr.conf.jinja
@@ -1,4 +1,6 @@
+{% include "bgpd.jinja" %}
{% include "isisd.jinja" %}
+{% include "prefix_lists.jinja" %}
{% for line in custom_frr_config %}
{{ line }}
{% endfor %}
@@ -6,4 +8,5 @@
{% include "ospfd.jinja" %}
{% include "access_lists.jinja" %}
{% include "route_maps.jinja" %}
+{% include "ip_routes.jinja" %}
{% include "protocol_routemaps.jinja" %}
diff --git a/proxmox-frr-templates/templates/ip_routes.jinja b/proxmox-frr-templates/templates/ip_routes.jinja
new file mode 100644
index 000000000000..3e33a709e821
--- /dev/null
+++ b/proxmox-frr-templates/templates/ip_routes.jinja
@@ -0,0 +1,8 @@
+{% for ip_route in ip_routes %}
+!
+{% if ip_route.vrf %}
+{{ "ipv6" if ip_route.is_ipv6 else "ip" }} route {{ ip_route.prefix }} {{ ip_route.via }} {{ ip_route.vrf }}
+{% else %}
+{{ "ipv6" if ip_route.is_ipv6 else "ip" }} route {{ ip_route.prefix }} {{ ip_route.via }}
+{% endif %}
+{% endfor %}
diff --git a/proxmox-frr-templates/templates/prefix_lists.jinja b/proxmox-frr-templates/templates/prefix_lists.jinja
new file mode 100644
index 000000000000..f431958af354
--- /dev/null
+++ b/proxmox-frr-templates/templates/prefix_lists.jinja
@@ -0,0 +1,6 @@
+{% for name, prefix_list in prefix_lists | items %}
+!
+{% for rule in prefix_list %}
+{{ "ipv6" if rule.is_ipv6 else "ip" }} prefix-list {{ name }} {{ ("seq " ~ rule.seq ~ " ") if rule.seq }}{{ rule.action }} {{ rule.network }}{{ (" le " ~ rule.le) if rule.le }}{{ (" ge " ~ rule.ge) if rule.ge }}
+{% endfor %}
+{% endfor %}
diff --git a/proxmox-frr/src/ser/bgp.rs b/proxmox-frr/src/ser/bgp.rs
new file mode 100644
index 000000000000..c0433d643207
--- /dev/null
+++ b/proxmox-frr/src/ser/bgp.rs
@@ -0,0 +1,185 @@
+use std::net::{IpAddr, Ipv4Addr};
+
+use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr};
+use serde::{Deserialize, Serialize};
+
+use crate::ser::route_map::RouteMapName;
+use crate::ser::{FrrWord, InterfaceName, IpRoute};
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct BgpRouterName {
+ asn: u32,
+ vrf: Option<FrrWord>,
+}
+
+impl BgpRouterName {
+ pub fn new(asn: u32, vrf: Option<FrrWord>) -> Self {
+ Self { asn, vrf }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum NeighborRemoteAs {
+ Internal,
+ External,
+ #[serde(untagged)]
+ Asn(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct NeighborGroup {
+ pub name: FrrWord,
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub bfd: bool,
+ pub local_as: Option<u32>,
+ pub remote_as: NeighborRemoteAs,
+ #[serde(default)]
+ pub ips: Vec<IpAddr>,
+ #[serde(default)]
+ pub interfaces: Vec<InterfaceName>,
+ pub ebgp_multihop: Option<i32>,
+ pub update_source: Option<InterfaceName>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct Ipv4UnicastAF {
+ #[serde(flatten)]
+ pub common_options: CommonAddressFamilyOptions,
+ #[serde(default)]
+ pub networks: Vec<Ipv4Cidr>,
+ #[serde(default)]
+ pub redistribute: Vec<Redistribution>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct Ipv6UnicastAF {
+ #[serde(flatten)]
+ pub common_options: CommonAddressFamilyOptions,
+ #[serde(default)]
+ pub networks: Vec<Ipv6Cidr>,
+ #[serde(default)]
+ pub redistribute: Vec<Redistribution>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct L2vpnEvpnAF {
+ #[serde(flatten)]
+ pub common_options: CommonAddressFamilyOptions,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub advertise_all_vni: Option<bool>,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub advertise_default_gw: Option<bool>,
+ #[serde(default)]
+ pub default_originate: Vec<DefaultOriginate>,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub advertise_ipv4_unicast: Option<bool>,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub advertise_ipv6_unicast: Option<bool>,
+ pub autort_as: Option<i32>,
+ pub route_targets: Option<RouteTargets>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum DefaultOriginate {
+ Ipv4,
+ Ipv6,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum RedistributeProtocol {
+ Connected,
+ Static,
+ Ospf,
+ Kernel,
+ Isis,
+ Ospf6,
+ Openfabric,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct Redistribution {
+ pub protocol: RedistributeProtocol,
+ pub metric: Option<u32>,
+ pub route_map: Option<RouteMapName>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct RouteTargets {
+ #[serde(default)]
+ import: Vec<FrrWord>,
+ #[serde(default)]
+ export: Vec<FrrWord>,
+ #[serde(default)]
+ both: Vec<FrrWord>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct AddressFamilyNeighbor {
+ pub name: String,
+ #[serde(
+ default,
+ skip_serializing_if = "Option::is_none",
+ deserialize_with = "proxmox_serde::perl::deserialize_bool"
+ )]
+ pub soft_reconfiguration_inbound: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub route_map_in: Option<RouteMapName>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub route_map_out: Option<RouteMapName>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct CommonAddressFamilyOptions {
+ #[serde(default)]
+ pub import_vrf: Vec<FrrWord>,
+ #[serde(default)]
+ pub neighbors: Vec<AddressFamilyNeighbor>,
+ #[serde(default)]
+ pub custom_frr_config: Vec<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Default)]
+pub struct AddressFamilies {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ ipv4_unicast: Option<Ipv4UnicastAF>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ ipv6_unicast: Option<Ipv6UnicastAF>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ l2vpn_evpn: Option<L2vpnEvpnAF>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct Vrf {
+ pub vni: Option<u32>,
+ #[serde(default)]
+ pub ip_routes: Vec<IpRoute>,
+ #[serde(default)]
+ pub custom_frr_config: Vec<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct BgpRouter {
+ pub asn: u32,
+ pub router_id: Ipv4Addr,
+ #[serde(default)]
+ pub coalesce_time: Option<u32>,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub default_ipv4_unicast: Option<bool>,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub hard_administrative_reset: Option<bool>,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub graceful_restart_notification: Option<bool>,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub disable_ebgp_connected_route_check: Option<bool>,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub bestpath_as_path_multipath_relax: Option<bool>,
+ #[serde(default)]
+ pub neighbor_groups: Vec<NeighborGroup>,
+ #[serde(default)]
+ pub address_families: AddressFamilies,
+ #[serde(default)]
+ pub custom_frr_config: Vec<String>,
+}
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
index 44a54586c7c5..1bd2d58fde19 100644
--- a/proxmox-frr/src/ser/mod.rs
+++ b/proxmox-frr/src/ser/mod.rs
@@ -1,3 +1,4 @@
+pub mod bgp;
pub mod isis;
pub mod openfabric;
pub mod ospf;
@@ -8,7 +9,9 @@ use std::collections::BTreeMap;
use std::net::IpAddr;
use std::str::FromStr;
-use crate::ser::route_map::{AccessListName, AccessListRule, RouteMapEntry, RouteMapName};
+use crate::ser::route_map::{
+ AccessListName, AccessListRule, PrefixListName, PrefixListRule, RouteMapEntry, RouteMapName,
+};
use proxmox_network_types::ip_address::Cidr;
use serde::{Deserialize, Serialize};
@@ -169,6 +172,14 @@ pub struct IpProtocolRouteMap {
pub v6: Option<RouteMapName>,
}
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub enum VrfName {
+ #[serde(rename = "default")]
+ Default,
+ #[serde(untagged)]
+ Custom(String),
+}
+
/// Main FRR config.
///
/// Contains the two main frr building blocks: routers and interfaces. It also holds other
@@ -180,14 +191,21 @@ pub struct FrrConfig {
#[serde(default)]
pub ospf: OspfFrrConfig,
#[serde(default)]
+ pub bgp: BgpFrrConfig,
+ #[serde(default)]
pub isis: IsisFrrConfig,
+ #[serde(default)]
+ pub ip_routes: Vec<IpRoute>,
#[serde(default)]
pub protocol_routemaps: BTreeMap<FrrProtocol, IpProtocolRouteMap>,
#[serde(default)]
pub routemaps: BTreeMap<RouteMapName, Vec<RouteMapEntry>>,
#[serde(default)]
pub access_lists: BTreeMap<AccessListName, Vec<AccessListRule>>,
+ #[serde(default)]
+ pub prefix_lists: BTreeMap<PrefixListName, Vec<PrefixListRule>>,
+
#[serde(default)]
pub custom_frr_config: Vec<String>,
}
@@ -215,3 +233,14 @@ pub struct OspfFrrConfig {
#[serde(default)]
pub interfaces: BTreeMap<InterfaceName, Interface<ospf::OspfInterface>>,
}
+
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub struct BgpFrrConfig {
+ #[serde(default)]
+ pub vrf_router: BTreeMap<VrfName, bgp::BgpRouter>,
+ #[serde(default)]
+ pub view_router: BTreeMap<u32, bgp::BgpRouter>,
+
+ #[serde(default)]
+ pub vrfs: BTreeMap<InterfaceName, bgp::Vrf>,
+}
diff --git a/proxmox-frr/src/ser/serializer.rs b/proxmox-frr/src/ser/serializer.rs
index 1f5f4897fea3..1eb5bec25e2d 100644
--- a/proxmox-frr/src/ser/serializer.rs
+++ b/proxmox-frr/src/ser/serializer.rs
@@ -5,11 +5,15 @@ use crate::ser::FrrConfig;
pub static TEMPLATES: phf::Map<&'static str, &'static str> = phf::phf_map! {
"fabricd.jinja" => include_str!("/usr/share/proxmox-frr/templates/fabricd.jinja"),
+ "bgpd.jinja" => include_str!("/usr/share/proxmox-frr/templates/bgpd.jinja"),
"isisd.jinja" => include_str!("/usr/share/proxmox-frr/templates/isisd.jinja"),
"ospfd.jinja" => include_str!("/usr/share/proxmox-frr/templates/ospfd.jinja"),
+ "bgp_router.jinja" => include_str!("/usr/share/proxmox-frr/templates/bgp_router.jinja"),
"interface.jinja" => include_str!("/usr/share/proxmox-frr/templates/interface.jinja"),
"access_lists.jinja" => include_str!("/usr/share/proxmox-frr/templates/access_lists.jinja"),
+ "prefix_lists.jinja" => include_str!("/usr/share/proxmox-frr/templates/prefix_lists.jinja"),
"route_maps.jinja" => include_str!("/usr/share/proxmox-frr/templates/route_maps.jinja"),
+ "ip_routes.jinja" => include_str!("/usr/share/proxmox-frr/templates/ip_routes.jinja"),
"protocol_routemaps.jinja" => include_str!("/usr/share/proxmox-frr/templates/protocol_routemaps.jinja"),
"frr.conf.jinja" => include_str!("/usr/share/proxmox-frr/templates/frr.conf.jinja"),
};
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH proxmox-perl-rs v2 1/1] sdn: add function to generate the frr config for all daemons
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (7 preceding siblings ...)
2026-03-02 12:55 ` [PATCH proxmox-ve-rs v2 8/8] frr: add bgp support with templates and serialization Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH pve-network v2 1/9] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
` (9 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
Add a function to generate the raw frr configuration for all the
protocols. This means we need a single function which takes the
fabric-config and the bgp+isis configuration. This function takes the
FrrConfig, which is partially filled with the bgp+isis config, and then
converts the fabrics to frr-rust-types and adds them to the FrrConfig.
Finally the FrrConfig is converted to literal frr-config-lines and
returned.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
pve-rs/Makefile | 1 +
pve-rs/src/bindings/sdn/fabrics.rs | 25 +++----------------------
pve-rs/src/bindings/sdn/mod.rs | 28 ++++++++++++++++++++++++++++
3 files changed, 32 insertions(+), 22 deletions(-)
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index aa7181efc78b..a4603365e3f7 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -31,6 +31,7 @@ PERLMOD_PACKAGES := \
PVE::RS::OpenId \
PVE::RS::ResourceScheduling::Static \
PVE::RS::SDN::Fabrics \
+ PVE::RS::SDN \
PVE::RS::TFA
PERLMOD_PACKAGE_FILES := $(addsuffix .pm,$(subst ::,/,$(PERLMOD_PACKAGES)))
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 54606e7b6bb9..18848c405201 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -14,10 +14,11 @@ pub mod pve_rs_sdn_fabrics {
use anyhow::{Context, Error, format_err};
use openssl::hash::{MessageDigest, hash};
+ use proxmox_ve_config::sdn::fabric::section_config::node::api::{Node, NodeUpdater};
use serde::{Deserialize, Serialize};
use perlmod::Value;
- use proxmox_frr::ser::serializer::to_raw_config;
+
use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
use proxmox_section_config::typed::SectionConfigData;
use proxmox_ve_config::common::valid::{Valid, Validatable};
@@ -29,12 +30,8 @@ pub mod pve_rs_sdn_fabrics {
api::{Fabric, FabricUpdater},
};
use proxmox_ve_config::sdn::fabric::section_config::interface::InterfaceName;
- use proxmox_ve_config::sdn::fabric::section_config::node::{
- Node as ConfigNode, NodeId,
- api::{Node, NodeUpdater},
- };
+ use proxmox_ve_config::sdn::fabric::section_config::node::{Node as ConfigNode, NodeId};
use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry};
- use proxmox_ve_config::sdn::frr::FrrConfigBuilder;
use crate::sdn::status::{self, RunningConfig};
@@ -462,22 +459,6 @@ pub mod pve_rs_sdn_fabrics {
daemons.into_iter().map(String::from).collect()
}
- /// Method: Return the FRR configuration for this config instance, as an array of
- /// strings, where each line represents a line in the FRR configuration.
- #[export]
- pub fn get_frr_raw_config(
- #[try_from_ref] this: &PerlFabricConfig,
- node_id: NodeId,
- ) -> Result<Vec<String>, Error> {
- let config = this.fabric_config.lock().unwrap();
-
- let frr_config = FrrConfigBuilder::default()
- .add_fabrics(config.clone().into_valid()?)
- .build(node_id)?;
-
- to_raw_config(&frr_config)
- }
-
/// Helper function to generate the default `/etc/network/interfaces` config for a given CIDR.
fn render_interface(name: &str, cidr: Cidr, is_dummy: bool) -> Result<String, Error> {
let mut interface = String::new();
diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs
index 0ec7009cc788..fde3138d55f7 100644
--- a/pve-rs/src/bindings/sdn/mod.rs
+++ b/pve-rs/src/bindings/sdn/mod.rs
@@ -1 +1,29 @@
pub(crate) mod fabrics;
+
+#[perlmod::package(name = "PVE::RS::SDN", lib = "pve_rs")]
+pub mod pve_rs_sdn {
+ //! The `PVE::RS::SDN` package.
+ //!
+ //! This provides general methods for generating the frr config.
+
+ use anyhow::Error;
+ use proxmox_frr::ser::{FrrConfig, serializer::to_raw_config};
+
+ use proxmox_ve_config::common::valid::Validatable;
+ use proxmox_ve_config::sdn::fabric::section_config::node::NodeId;
+
+ use crate::bindings::pve_rs_sdn_fabrics::PerlFabricConfig;
+
+ /// Return the FRR configuration for the passed FrrConfig and the FabricsConfig as an array of
+ /// strings, where each line represents a line in the FRR configuration.
+ #[export]
+ pub fn get_frr_raw_config(
+ mut frr_config: FrrConfig,
+ #[try_from_ref] cfg: &PerlFabricConfig,
+ node_id: NodeId,
+ ) -> Result<Vec<String>, Error> {
+ let fabric_config = cfg.fabric_config.lock().unwrap().clone().into_valid()?;
+ proxmox_ve_config::sdn::fabric::frr::build_fabric(node_id, fabric_config, &mut frr_config)?;
+ to_raw_config(&frr_config)
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH pve-network v2 1/9] sdn: remove duplicate comment line '!' in frr config
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (8 preceding siblings ...)
2026-03-02 12:55 ` [PATCH proxmox-perl-rs v2 1/1] sdn: add function to generate the frr config for all daemons Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH pve-network v2 2/9] sdn: tests: add missing comment " Gabriel Goller
` (8 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 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 6d5f43084412..9f81369c079b 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -230,7 +230,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 10a601eb8d09..1b6145601b08 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 d67552a9ed6b..9ee41a9cd637 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 ad9753919e6f..f9472b314b3e 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 22f1b10fa906..38f62d06540c 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 160a0a0528ff..cc3ae9c4b3ae 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 50f226b51e98..8dc8c6b044c9 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 1397160af3d1..f11e3ba0258e 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 160a0a0528ff..cc3ae9c4b3ae 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 781e6e21a8e9..3e95d77b7984 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 d67552a9ed6b..9ee41a9cd637 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 d67552a9ed6b..9ee41a9cd637 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 d67552a9ed6b..9ee41a9cd637 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 d67552a9ed6b..9ee41a9cd637 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 6f453be54723..ddc5338f02b0 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 e47afaa6d556..2361879e0cc2 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 487ca86b8d8b..f4a6e353377b 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 92a6f3843646..2cf4dcbf359a 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 4a4617bdd1d5..783d05cf24d5 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 ab3d85142315..6096221b2eef 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 397655e34722..a96adc329c5f 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 dd0e9d8a5573..bb9313244d53 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 c5b65724b9b6..367aaf3d2671 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 d67552a9ed6b..9ee41a9cd637 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
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH pve-network v2 2/9] sdn: tests: add missing comment '!' in frr config
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (9 preceding siblings ...)
2026-03-02 12:55 ` [PATCH pve-network v2 1/9] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH pve-network v2 3/9] tests: use Test::Differences to make test assertions Gabriel Goller
` (7 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 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 a96adc329c5f..f3d82ef242be 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 bb9313244d53..3e41a56eeb9d 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
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH pve-network v2 3/9] tests: use Test::Differences to make test assertions
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (10 preceding siblings ...)
2026-03-02 12:55 ` [PATCH pve-network v2 2/9] sdn: tests: add missing comment " Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH pve-network v2 4/9] sdn: write structured frr config that can be rendered using templates Gabriel Goller
` (6 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 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 | 23 +++++++++++++--------
src/test/run_test_zones.pl | 5 +++--
6 files changed, 50 insertions(+), 38 deletions(-)
diff --git a/debian/control b/debian/control
index ee7b2d40dfe2..6fe98822e64c 100644
--- a/debian/control
+++ b/debian/control
@@ -9,6 +9,7 @@ Build-Depends: debhelper-compat (= 13),
libnet-subnet-perl,
libpve-rs-perl (>= 0.11.1),
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..9f4c424f3d2f 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,20 @@ 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
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH pve-network v2 4/9] sdn: write structured frr config that can be rendered using templates
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (11 preceding siblings ...)
2026-03-02 12:55 ` [PATCH pve-network v2 3/9] tests: use Test::Differences to make test assertions Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH pve-network v2 5/9] tests: rearrange some statements in the frr config Gabriel Goller
` (5 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
The structured frr config can be deserialized by rust and rendered using
the templates (isis and bgp) in proxmox-frr.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
src/PVE/Network/SDN.pm | 11 +-
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 104 ++---
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 378 +++++++++---------
src/PVE/Network/SDN/Controllers/IsisPlugin.pm | 28 +-
src/PVE/Network/SDN/Fabrics.pm | 14 +-
src/PVE/Network/SDN/Frr.pm | 164 +-------
6 files changed, 282 insertions(+), 417 deletions(-)
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index c7c390e80586..c000bed498ec 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -419,15 +419,16 @@ sub generate_frr_raw_config {
$fabric_config = PVE::Network::SDN::Fabrics::config(1) if !$fabric_config;
my $frr_config = {};
+
PVE::Network::SDN::Controllers::generate_frr_config($frr_config, $running_config);
PVE::Network::SDN::Frr::append_local_config($frr_config);
+ PVE::Network::SDN::Frr::fix_routemap_seqs($frr_config);
- my $raw_config = PVE::Network::SDN::Frr::to_raw_config($frr_config);
-
- my $fabrics_config = PVE::Network::SDN::Fabrics::generate_frr_raw_config($fabric_config);
- push @$raw_config, @$fabrics_config;
+ my $nodename = PVE::INotify::nodename();
- return $raw_config;
+ return PVE::RS::SDN::get_frr_raw_config(
+ $frr_config->{'frr'}, $fabric_config, $nodename,
+ );
}
=head3 get_frr_daemon_status(\%fabric_config)
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
index 447ebf1ba744..8891541219f6 100644
--- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
@@ -62,7 +62,7 @@ sub generate_frr_config {
my @peers;
@peers = PVE::Tools::split_list($plugin_config->{'peers'}) if $plugin_config->{'peers'};
- my $asn = $plugin_config->{asn};
+ my $asn = int($plugin_config->{asn});
my $ebgp = $plugin_config->{ebgp};
my $ebgp_multihop = $plugin_config->{'ebgp-multihop'};
my $loopback = $plugin_config->{loopback};
@@ -73,66 +73,80 @@ sub generate_frr_config {
return if !$asn;
return if $local_node ne $plugin_config->{node};
- my $bgp = $config->{frr}->{router}->{"bgp $asn"} //= {};
-
my ($ifaceip, $interface) =
PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback);
my $routerid = PVE::Network::SDN::Controllers::Plugin::get_router_id($ifaceip, $interface);
- my $remoteas = $ebgp ? "external" : $asn;
-
- #global options
- my @controller_config = (
- "bgp router-id $routerid", "no bgp default ipv4-unicast", "coalesce-time 1000",
- );
-
- push(@{ $bgp->{""} }, @controller_config) if keys %{$bgp} == 0;
+ my $bgp_router = $config->{frr}->{bgp}->{vrf_router}->{'default'} //= {};
- @controller_config = ();
- if ($ebgp) {
- push @controller_config, "bgp disable-ebgp-connected-route-check" if $loopback;
+ # Initialize router if not already configured
+ if (!keys %{$bgp_router}) {
+ $bgp_router->{asn} = $asn;
+ $bgp_router->{router_id} = $routerid;
+ $bgp_router->{default_ipv4_unicast} = 0;
+ $bgp_router->{coalesce_time} = 1000;
+ $bgp_router->{neighbor_groups} = [];
+ $bgp_router->{address_families} = {};
}
- push @controller_config, "bgp bestpath as-path multipath-relax" if $multipath_relax;
+ # Add BGP-specific options
+ $bgp_router->{disable_ebgp_connected_route_check} = 1 if $loopback && $ebgp;
+ $bgp_router->{bestpath_as_path_multipath_relax} = 1 if $multipath_relax;
- #BGP neighbors
- if (@peers) {
- push @controller_config, "neighbor BGP peer-group";
- push @controller_config, "neighbor BGP remote-as $remoteas";
- push @controller_config, "neighbor BGP bfd";
- push @controller_config, "neighbor BGP ebgp-multihop $ebgp_multihop"
- if $ebgp && $ebgp_multihop;
- }
-
- # BGP peers
- foreach my $address (@peers) {
- push @controller_config, "neighbor $address peer-group BGP";
- }
- push(@{ $bgp->{""} }, @controller_config);
-
- # address-family unicast
+ # Build BGP neighbor group
if (@peers) {
+ my $neighbor_group = {
+ name => "BGP",
+ bfd => 1,
+ remote_as => $ebgp ? "external" : $asn,
+ ips => \@peers,
+ interfaces => [],
+ };
+ $neighbor_group->{ebgp_multihop} = int($ebgp_multihop) if $ebgp && $ebgp_multihop;
+
+ push @{ $bgp_router->{neighbor_groups} }, $neighbor_group;
+
+ # Configure address-family unicast
my $ipversion = Net::IP::ip_is_ipv6($ifaceip) ? "ipv6" : "ipv4";
my $mask = Net::IP::ip_is_ipv6($ifaceip) ? "128" : "32";
+ my $af_key = "${ipversion}_unicast";
+
+ $bgp_router->{address_families}->{$af_key} //= {
+ networks => [],
+ neighbors => [{
+ name => "BGP",
+ soft_reconfiguration_inbound => 1,
+ }],
+ };
- push(@{ $bgp->{"address-family"}->{"$ipversion unicast"} }, "network $ifaceip/$mask")
+ push @{ $bgp_router->{address_families}->{$af_key}->{networks} }, "$ifaceip/$mask"
if $loopback;
- push(@{ $bgp->{"address-family"}->{"$ipversion unicast"} }, "neighbor BGP activate");
- push(
- @{ $bgp->{"address-family"}->{"$ipversion unicast"} },
- "neighbor BGP soft-reconfiguration inbound",
- );
}
+ # Configure route-map for source IP correction with loopback
if ($loopback) {
- $config->{frr_prefix_list}->{loopbacks_ips}->{10} = "permit 0.0.0.0/0 le 32";
- push(@{ $config->{frr_ip_protocol} }, "ip protocol bgp route-map correct_src");
-
- my $routemap_config = ();
- push @{$routemap_config}, "match ip address prefix-list loopbacks_ips";
- push @{$routemap_config}, "set src $ifaceip";
- my $routemap = { rule => $routemap_config, action => "permit" };
- push(@{ $config->{frr_routemap}->{'correct_src'} }, $routemap);
+ $config->{frr}->{prefix_lists}->{loopbacks_ips} = [{
+ seq => 10,
+ action => 'permit',
+ network => '0.0.0.0/0',
+ le => 32,
+ is_ipv6 => 0,
+ }];
+
+ $config->{frr}->{protocol_routemaps}->{bgp}->{v4} = "correct_src";
+
+ my $routemap_config = {
+ protocol_type => 'ip',
+ match_type => 'address',
+ value => { list_type => 'prefixlist', list_name => 'loopbacks_ips' },
+ };
+ my $routemap = {
+ matches => [$routemap_config],
+ sets => [{ set_type => 'src', value => $ifaceip }],
+ action => "permit",
+ seq => 1,
+ };
+ push(@{ $config->{frr}->{routemaps}->{'correct_src'} }, $routemap);
}
return $config;
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index cc217126607f..e3091c63ac8d 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -55,15 +55,15 @@ sub generate_frr_config {
my $local_node = PVE::INotify::nodename();
my @peers;
- my $asn = $plugin_config->{asn};
+ my $asn = int($plugin_config->{asn});
my $ebgp = undef;
my $loopback = undef;
my $autortas = undef;
my $ifaceip = undef;
my $routerid = undef;
- my $bgprouter = find_bgp_controller($local_node, $controller_cfg);
- my $isisrouter = find_isis_controller($local_node, $controller_cfg);
+ my $bgp_controller = find_bgp_controller($local_node, $controller_cfg);
+ my $isis_controller = find_isis_controller($local_node, $controller_cfg);
if ($plugin_config->{'fabric'}) {
my $config = PVE::Network::SDN::Fabrics::config(1);
@@ -102,10 +102,10 @@ sub generate_frr_config {
} elsif ($plugin_config->{'peers'}) {
@peers = PVE::Tools::split_list($plugin_config->{'peers'});
- if ($bgprouter) {
- $loopback = $bgprouter->{loopback} if $bgprouter->{loopback};
- } elsif ($isisrouter) {
- $loopback = $isisrouter->{loopback} if $isisrouter->{loopback};
+ if ($bgp_controller) {
+ $loopback = $bgp_controller->{loopback} if $bgp_controller->{loopback};
+ } elsif ($isis_controller) {
+ $loopback = $isis_controller->{loopback} if $isis_controller->{loopback};
}
($ifaceip, my $interface) =
@@ -116,58 +116,60 @@ sub generate_frr_config {
return;
}
- if ($bgprouter) {
- $ebgp = 1 if $plugin_config->{'asn'} ne $bgprouter->{asn};
- $asn = $bgprouter->{asn} if $bgprouter->{asn};
+ if ($bgp_controller) {
+ $ebgp = 1 if $plugin_config->{'asn'} ne $bgp_controller->{asn};
+ $asn = $bgp_controller->{asn} if $bgp_controller->{asn};
$autortas = $plugin_config->{'asn'} if $ebgp;
}
return if !$asn || !$routerid;
- my $bgp = $config->{frr}->{router}->{"bgp $asn"} //= {};
-
- my $remoteas = $ebgp ? "external" : $asn;
-
- #global options
- my @controller_config = (
- "bgp router-id $routerid",
- "no bgp hard-administrative-reset",
- "no bgp default ipv4-unicast",
- "coalesce-time 1000",
- "no bgp graceful-restart notification",
- );
-
- push(@{ $bgp->{""} }, @controller_config) if keys %{$bgp} == 0;
- @controller_config = ();
-
- #VTEP neighbors
- push @controller_config, "neighbor VTEP peer-group";
- push @controller_config, "neighbor VTEP remote-as $remoteas";
- push @controller_config, "neighbor VTEP bfd";
+ my $bgp_router = $config->{frr}->{bgp}->{vrf_router}->{'default'} //= {};
+
+ # Initialize router if not already configured
+ if (!keys %{$bgp_router}) {
+ $bgp_router->{asn} = $asn;
+ $bgp_router->{router_id} = $routerid;
+ $bgp_router->{default_ipv4_unicast} = 0;
+ $bgp_router->{hard_administrative_reset} = 0;
+ $bgp_router->{graceful_restart_notification} = 0;
+ $bgp_router->{coalesce_time} = 1000;
+ $bgp_router->{neighbor_groups} = [];
+ $bgp_router->{address_families} = {};
+ }
- push @controller_config, "neighbor VTEP ebgp-multihop 10" if $ebgp && $loopback;
- push @controller_config, "neighbor VTEP update-source $loopback" if $loopback;
+ # Build VTEP neighbor group
+ my @vtep_ips = grep { $_ ne $ifaceip } @peers;
- # VTEP peers
- foreach my $address (@peers) {
- next if $address eq $ifaceip;
- push @controller_config, "neighbor $address peer-group VTEP";
- }
+ my $neighbor_group = {
+ name => "VTEP",
+ bfd => 1,
+ remote_as => $ebgp ? "external" : $asn,
+ ips => \@vtep_ips,
+ interfaces => [],
+ };
+ $neighbor_group->{ebgp_multihop} = 10 if $ebgp && $loopback;
+ $neighbor_group->{update_source} = $loopback if $loopback;
+
+ push @{ $bgp_router->{neighbor_groups} }, $neighbor_group;
+
+ # Configure l2vpn evpn address family
+ $bgp_router->{address_families}->{l2vpn_evpn} //= {
+ neighbors => [{
+ name => "VTEP",
+ route_map_in => 'MAP_VTEP_IN',
+ route_map_out => 'MAP_VTEP_OUT',
+ }],
+ advertise_all_vni => 1,
+ };
- push(@{ $bgp->{""} }, @controller_config);
+ $bgp_router->{address_families}->{l2vpn_evpn}->{autort_as} = $autortas if $autortas;
- # address-family l2vpn
- @controller_config = ();
- push @controller_config, "neighbor VTEP activate";
- push @controller_config, "neighbor VTEP route-map MAP_VTEP_IN in";
- push @controller_config, "neighbor VTEP route-map MAP_VTEP_OUT out";
- push @controller_config, "advertise-all-vni";
- push @controller_config, "autort as $autortas" if $autortas;
- push(@{ $bgp->{"address-family"}->{"l2vpn evpn"} }, @controller_config);
+ my $routemap_in = { seq => 1, action => "permit" };
+ my $routemap_out = { seq => 1, action => "permit" };
- my $routemap = { rule => undef, action => "permit" };
- push(@{ $config->{frr_routemap}->{'MAP_VTEP_IN'} }, $routemap);
- push(@{ $config->{frr_routemap}->{'MAP_VTEP_OUT'} }, $routemap);
+ push($config->{frr}->{routemaps}->{'MAP_VTEP_IN'}->@*, $routemap_in);
+ push($config->{frr}->{routemaps}->{'MAP_VTEP_OUT'}->@*, $routemap_out);
return $config;
}
@@ -260,11 +262,19 @@ sub generate_zone_frr_config {
my $is_gateway = $exitnodes->{$local_node};
- # vrf
- my @controller_config = ();
- push @controller_config, "vni $vrfvxlan";
- #avoid to routes between nodes through the exit nodes
- #null routes subnets of other zones
+ # Configure VRF
+ my $vrf_router = $config->{frr}->{bgp}->{vrf_router}->{$vrf} //= {};
+ $vrf_router->{asn} = $asn;
+ $vrf_router->{router_id} = $routerid;
+ $vrf_router->{hard_administrative_reset} = 0;
+ $vrf_router->{graceful_restart_notification} = 0;
+
+ my $bgp_vrf = $config->{frr}->{bgp}->{vrfs}->{$vrf} //= {};
+
+ $bgp_vrf->{vni} = $vrfvxlan;
+ $bgp_vrf->{ip_routes} = [];
+
+ # Add null routes for other zones to avoid routing between nodes through exit nodes
if ($is_gateway) {
my $subnets = PVE::Network::SDN::Vnets::get_subnets();
my $cidrs = {};
@@ -283,162 +293,135 @@ sub generate_zone_frr_config {
keys $cidrs->%*;
foreach my $ip (@sorted_ip) {
- my $ipversion = Net::IP::ip_is_ipv4($ip) ? 'ip' : 'ipv6';
- push @controller_config, "$ipversion route $ip/$cidrs->{$ip} null0";
+ my $is_ipv6 = Net::IP::ip_is_ipv6($ip);
+ push @{ $bgp_vrf->{ip_routes} },
+ {
+ is_ipv6 => $is_ipv6,
+ prefix => "$ip/$cidrs->{$ip}",
+ via => "null0",
+ };
}
}
- push(@{ $config->{frr}->{vrf}->{"$vrf"} }, @controller_config);
-
- #main vrf router
- @controller_config = ();
- push @controller_config, "bgp router-id $routerid";
- push @controller_config, "no bgp hard-administrative-reset";
- push @controller_config, "no bgp graceful-restart notification";
-
- # push @controller_config, "!";
- push(@{ $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{""} }, @controller_config);
+ # Configure VRF BGP router
+ $vrf_router->{neighbor_groups} = [];
+ $vrf_router->{address_families} = {};
+ # Configure L2VPN EVPN address family with route targets
if ($autortas) {
- push(
- @{
- $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
- ->{"l2vpn evpn"}
- },
- "route-target import $autortas:$vrfvxlan",
- );
- push(
- @{
- $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
- ->{"l2vpn evpn"}
- },
- "route-target export $autortas:$vrfvxlan",
- );
+ $vrf_router->{address_families}->{l2vpn_evpn} //= {};
+ $vrf_router->{address_families}->{l2vpn_evpn}->{route_targets} = {
+ import => ["$autortas:$vrfvxlan"],
+ export => ["$autortas:$vrfvxlan"],
+ };
}
if ($is_gateway) {
-
- $config->{frr_prefix_list}->{'only_default'}->{1} = "permit 0.0.0.0/0";
- $config->{frr_prefix_list_v6}->{'only_default_v6'}->{1} = "permit ::/0";
+ push(
+ @{ $config->{frr}->{prefix_lists}->{only_default} },
+ { seq => 1, action => 'permit', network => '0.0.0.0/0', is_ipv6 => 0 },
+ ) if !defined($config->{frr}->{prefix_lists}->{only_default});
+ push(
+ @{ $config->{frr}->{prefix_lists}->{only_default_v6} },
+ { seq => 1, action => 'permit', network => '::/0', is_ipv6 => 1 },
+ ) if !defined($config->{frr}->{prefix_lists}->{only_default_v6});
if (!$exitnodes_primary || $exitnodes_primary eq $local_node) {
- #filter default route coming from other exit nodes on primary node or both nodes if no primary is defined.
- my $routemap_config_v6 = ();
- push @{$routemap_config_v6}, "match ipv6 address prefix-list only_default_v6";
- my $routemap_v6 = { rule => $routemap_config_v6, action => "deny" };
- unshift(@{ $config->{frr_routemap}->{'MAP_VTEP_IN'} }, $routemap_v6);
+ # Filter default route coming from other exit nodes on primary node
+ my $routemap_config_v6 = {
+ protocol_type => 'ipv6',
+ match_type => 'address',
+ value => { list_type => 'prefixlist', list_name => 'only_default_v6' },
+ };
+ my $routemap_v6 = { seq => 1, matches => [$routemap_config_v6], action => "deny" };
+ unshift(
+ @{ $config->{frr}->{routemaps}->{'MAP_VTEP_IN'} }, $routemap_v6,
+ );
- my $routemap_config = ();
- push @{$routemap_config}, "match ip address prefix-list only_default";
- my $routemap = { rule => $routemap_config, action => "deny" };
- unshift(@{ $config->{frr_routemap}->{'MAP_VTEP_IN'} }, $routemap);
+ my $routemap_config = {
+ protocol_type => 'ip',
+ match_type => 'address',
+ value => { list_type => 'prefixlist', list_name => 'only_default' },
+ };
+ my $routemap = { seq => 1, matches => [$routemap_config], action => "deny" };
+ unshift(@{ $config->{frr}->{routemaps}->{'MAP_VTEP_IN'} }, $routemap);
} elsif ($exitnodes_primary ne $local_node) {
- my $routemap_config_v6 = ();
- push @{$routemap_config_v6}, "match ipv6 address prefix-list only_default_v6";
- push @{$routemap_config_v6}, "set metric 200";
- my $routemap_v6 = { rule => $routemap_config_v6, action => "permit" };
- unshift(@{ $config->{frr_routemap}->{'MAP_VTEP_OUT'} }, $routemap_v6);
-
- my $routemap_config = ();
- push @{$routemap_config}, "match ip address prefix-list only_default";
- push @{$routemap_config}, "set metric 200";
- my $routemap = { rule => $routemap_config, action => "permit" };
- unshift(@{ $config->{frr_routemap}->{'MAP_VTEP_OUT'} }, $routemap);
+ my $routemap_config_v6 = {
+ protocol_type => 'ipv6',
+ match_type => 'address',
+ value => { list_type => 'prefixlist', list_name => 'only_default_v6' },
+ };
+ my $routemap_v6 = {
+ seq => 1,
+ matches => [$routemap_config_v6],
+ sets => [{ set_type => 'metric', value => 200 }],
+ action => "permit",
+ };
+ unshift(
+ @{ $config->{frr}->{routemaps}->{'MAP_VTEP_OUT'} }, $routemap_v6,
+ );
+
+ my $routemap_config = {
+ protocol_type => 'ip',
+ match_type => 'address',
+ value => { list_type => 'prefixlist', list_name => 'only_default' },
+ };
+ my $routemap = {
+ seq => 1,
+ matches => [$routemap_config],
+ sets => [{ set_type => 'metric', value => 200 }],
+ action => "permit",
+ };
+ unshift(@{ $config->{frr}->{routemaps}->{'MAP_VTEP_OUT'} }, $routemap);
}
if (!$exitnodes_local_routing) {
- @controller_config = ();
- #import /32 routes of evpn network from vrf1 to default vrf (for packet return)
- push @controller_config, "import vrf $vrf";
- push(
- @{
- $config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv4 unicast"}
- },
- @controller_config,
- );
- push(
- @{
- $config->{frr}->{router}->{"bgp $asn"}->{"address-family"}->{"ipv6 unicast"}
- },
- @controller_config,
- );
-
- @controller_config = ();
- #redistribute connected to be able to route to local vms on the gateway
- push @controller_config, "redistribute connected";
- push(
- @{
- $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
- ->{"ipv4 unicast"}
- },
- @controller_config,
- );
- push(
- @{
- $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
- ->{"ipv6 unicast"}
- },
- @controller_config,
- );
+ # Import /32 routes from VRF to main router
+ my $main_bgp_router = $config->{frr}->{bgp}->{vrf_router}->{'default'};
+ if ($main_bgp_router) {
+ $main_bgp_router->{address_families}->{ipv4_unicast} //= {};
+ push(@{ $main_bgp_router->{address_families}->{ipv4_unicast}->{import_vrf} }, $vrf);
+
+ $main_bgp_router->{address_families}->{ipv6_unicast} //= {};
+ push(@{ $main_bgp_router->{address_families}->{ipv6_unicast}->{import_vrf} }, $vrf);
+ }
+
+ # Redistribute connected in VRF router
+ $vrf_router->{address_families}->{ipv4_unicast} //= { redistribute => [] };
+ push @{ $vrf_router->{address_families}->{ipv4_unicast}->{redistribute} },
+ { protocol => "connected" };
+
+ $vrf_router->{address_families}->{ipv6_unicast} //= { redistribute => [] };
+ push @{ $vrf_router->{address_families}->{ipv6_unicast}->{redistribute} },
+ { protocol => "connected" };
}
- @controller_config = ();
- #add default originate to announce 0.0.0.0/0 type5 route in evpn
- push @controller_config, "default-originate ipv4";
- push @controller_config, "default-originate ipv6";
- push(
- @{
- $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
- ->{"l2vpn evpn"}
- },
- @controller_config,
- );
- } elsif ($advertisesubnets) {
+ # Add default originate to announce 0.0.0.0/0 type5 route in evpn
+ $vrf_router->{address_families}->{l2vpn_evpn} //= {};
+ $vrf_router->{address_families}->{l2vpn_evpn}->{default_originate} = ["ipv4", "ipv6"];
- @controller_config = ();
- #redistribute connected networks
- push @controller_config, "redistribute connected";
- push(
- @{
- $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
- ->{"ipv4 unicast"}
- },
- @controller_config,
- );
- push(
- @{
- $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
- ->{"ipv6 unicast"}
- },
- @controller_config,
- );
-
- @controller_config = ();
- #advertise connected networks type5 route in evpn
- push @controller_config, "advertise ipv4 unicast";
- push @controller_config, "advertise ipv6 unicast";
- push(
- @{
- $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
- ->{"l2vpn evpn"}
- },
- @controller_config,
- );
+ } elsif ($advertisesubnets) {
+ # Redistribute connected networks
+ $vrf_router->{address_families}->{ipv4_unicast} //= { redistribute => [] };
+ push @{ $vrf_router->{address_families}->{ipv4_unicast}->{redistribute} },
+ { protocol => "connected" };
+
+ $vrf_router->{address_families}->{ipv6_unicast} //= { redistribute => [] };
+ push @{ $vrf_router->{address_families}->{ipv6_unicast}->{redistribute} },
+ { protocol => "connected" };
+
+ # Advertise connected networks type5 route in evpn
+ $vrf_router->{address_families}->{l2vpn_evpn} //= {};
+ $vrf_router->{address_families}->{l2vpn_evpn}->{advertise_ipv4_unicast} = 1;
+ $vrf_router->{address_families}->{l2vpn_evpn}->{advertise_ipv6_unicast} = 1;
}
if ($rt_import) {
- @controller_config = ();
- foreach my $rt (sort @{$rt_import}) {
- push @controller_config, "route-target import $rt";
- }
- push(
- @{
- $config->{frr}->{router}->{"bgp $asn vrf $vrf"}->{"address-family"}
- ->{"l2vpn evpn"}
- },
- @controller_config,
- );
+ $vrf_router->{address_families}->{l2vpn_evpn} //= { route_targets => {} };
+ $vrf_router->{address_families}->{l2vpn_evpn}->{route_targets}->{import} //= [];
+ push @{ $vrf_router->{address_families}->{l2vpn_evpn}->{route_targets}->{import} },
+ @{$rt_import};
}
return $config;
@@ -458,18 +441,29 @@ sub generate_vnet_frr_config {
return if !$is_gateway;
my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1);
- my @controller_config = ();
+ $config->{frr}->{ip_routes} //= [];
foreach my $subnetid (sort keys %{$subnets}) {
my $subnet = $subnets->{$subnetid};
my $cidr = $subnet->{cidr};
my ($ip) = split(/\//, $cidr, 2);
if (Net::IP::ip_is_ipv6($ip)) {
- push @controller_config, "ipv6 route $cidr fe80::2 xvrf_$zoneid";
+ push @{ $config->{frr}->{ip_routes} },
+ {
+ prefix => $cidr,
+ via => "fe80::2",
+ vrf => "xvrf_$zoneid",
+ is_ipv6 => 1,
+ };
} else {
- push @controller_config, "ip route $cidr 10.255.255.2 xvrf_$zoneid";
+ push @{ $config->{frr}->{ip_routes} },
+ {
+ prefix => $cidr,
+ via => "10.255.255.2",
+ vrf => "xvrf_$zoneid",
+ is_ipv6 => 0,
+ };
}
}
- push(@{ $config->{frr_ip_protocol} }, @controller_config);
}
sub on_delete_hook {
diff --git a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
index 3a9acfda0744..454bdda6d316 100644
--- a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
@@ -69,23 +69,27 @@ sub generate_frr_config {
return if !$isis_ifaces || !$isis_net || !$isis_domain;
return if $local_node ne $plugin_config->{node};
- my @router_config = (
- "net $isis_net",
- "redistribute ipv4 connected level-1",
- "redistribute ipv6 connected level-1",
- "log-adjacency-changes",
- );
-
- push(@{ $config->{frr}->{router}->{"isis $isis_domain"} }, @router_config);
-
- my @iface_config = ("ip router isis $isis_domain");
+ # Configure IS-IS router
+ my $isis_router = $config->{frr}->{isis}->{router}->{$isis_domain} //= {};
+
+ $isis_router->{net} = $isis_net;
+ $isis_router->{log_adjacency_changes} = 1;
+ $isis_router->{redistribute} = {
+ ipv4_connected => "level-1",
+ ipv6_connected => "level-1",
+ };
+ # Configure interfaces
my $altnames = PVE::Network::altname_mapping();
-
my @ifaces = PVE::Tools::split_list($isis_ifaces);
+
+ $config->{frr}->{isis}->{interfaces} //= {};
for my $iface (sort @ifaces) {
my $iface_name = $altnames->{$iface} // $iface;
- push(@{ $config->{frr_interfaces}->{$iface_name} }, @iface_config);
+ $config->{frr}->{isis}->{interfaces}->{$iface_name} //= {};
+ $config->{frr}->{isis}->{interfaces}->{$iface_name}->{domain} = $isis_domain;
+ $config->{frr}->{isis}->{interfaces}->{$iface_name}->{is_ipv4} = 1;
+ $config->{frr}->{isis}->{interfaces}->{$iface_name}->{is_ipv6} = 0;
}
return $config;
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index d90992a7eceb..3ca362a02660 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -6,6 +6,7 @@ use warnings;
use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file cfs_write_file);
use PVE::JSONSchema qw(get_standard_option);
use PVE::INotify;
+use PVE::RS::SDN;
use PVE::RS::SDN::Fabrics;
PVE::JSONSchema::register_format(
@@ -100,19 +101,6 @@ sub get_frr_daemon_status {
return $daemon_status;
}
-sub generate_frr_raw_config {
- my ($fabric_config) = @_;
-
- my @raw_config = ();
-
- my $nodename = PVE::INotify::nodename();
-
- my $frr_config = $fabric_config->get_frr_raw_config($nodename);
- push @raw_config, @$frr_config if @$frr_config;
-
- return \@raw_config;
-}
-
sub generate_etc_network_config {
my $nodename = PVE::INotify::nodename();
my $fabric_config = PVE::Network::SDN::Fabrics::config(1);
diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
index 9f81369c079b..b572f4536004 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -187,28 +187,27 @@ sub set_daemon_status {
return $changed;
}
-=head3 to_raw_config(\%frr_config)
+=head3 fix_routemap_seqs(\$frr_config)
-Converts a given C<\%frr_config> to the raw config format.
+Iterates over all bgp route-maps in C<\$frr_config> and renumbers their sequence
+numbers to be consecutive, starting from 1 and incrementing by 1 for each entry.
=cut
-sub to_raw_config {
+sub fix_routemap_seqs {
my ($frr_config) = @_;
- my $raw_config = [];
+ my $routemaps = $frr_config->{'frr'}->{'routemaps'};
+ return if !defined($routemaps);
- generate_frr_vrf($raw_config, $frr_config->{frr}->{vrf});
- generate_frr_interfaces($raw_config, $frr_config->{frr_interfaces});
- generate_frr_recurse($raw_config, $frr_config->{frr}, undef, 0);
- generate_frr_list($raw_config, $frr_config->{frr_access_list}, "access-list");
- generate_frr_list($raw_config, $frr_config->{frr_prefix_list}, "ip prefix-list");
- generate_frr_list($raw_config, $frr_config->{frr_prefix_list_v6}, "ipv6 prefix-list");
- generate_frr_simple_list($raw_config, $frr_config->{frr_bgp_community_list});
- generate_frr_routemap($raw_config, $frr_config->{frr_routemap});
- generate_frr_simple_list($raw_config, $frr_config->{frr_ip_protocol});
-
- return $raw_config;
+ foreach my $id (sort keys %$routemaps) {
+ my $routemap = $routemaps->{$id};
+ my $order = 0;
+ foreach my $seq (@$routemap) {
+ $order++;
+ $seq->{seq} = $order;
+ }
+ }
}
=head3 raw_config_to_string(\@raw_config)
@@ -339,139 +338,4 @@ sub append_local_config {
}
}
-sub generate_frr_recurse {
- my ($final_config, $content, $parentkey, $level) = @_;
-
- my $keylist = {};
- $keylist->{'address-family'} = 1;
- $keylist->{router} = 1;
-
- my $exitkeylist = {};
- $exitkeylist->{'address-family'} = 1;
-
- my $simple_exitkeylist = {};
- $simple_exitkeylist->{router} = 1;
-
- # FIXME: make this generic
- my $paddinglevel = undef;
- if ($level == 1 || $level == 2) {
- $paddinglevel = $level - 1;
- } elsif ($level == 3 || $level == 4) {
- $paddinglevel = $level - 2;
- }
-
- my $padding = "";
- $padding = ' ' x ($paddinglevel) if $paddinglevel;
-
- if (ref $content eq 'HASH') {
- foreach my $key (sort keys %$content) {
- next if $key eq 'vrf';
- if ($parentkey && defined($keylist->{$parentkey})) {
- push @{$final_config}, $padding . "!";
- push @{$final_config}, $padding . "$parentkey $key";
- } elsif ($key ne '' && !defined($keylist->{$key})) {
- push @{$final_config}, $padding . "$key";
- }
-
- my $option = $content->{$key};
- generate_frr_recurse($final_config, $option, $key, $level + 1);
-
- push @{$final_config}, $padding . "exit-$parentkey"
- if $parentkey && defined($exitkeylist->{$parentkey});
- push @{$final_config}, $padding . "exit"
- if $parentkey && defined($simple_exitkeylist->{$parentkey});
- }
- }
-
- if (ref $content eq 'ARRAY') {
- push @{$final_config}, map { $padding . "$_" } @$content;
- }
-}
-
-sub generate_frr_vrf {
- my ($final_config, $vrfs) = @_;
-
- return if !$vrfs;
-
- my @config = ();
-
- foreach my $id (sort keys %$vrfs) {
- my $vrf = $vrfs->{$id};
- push @config, "!";
- push @config, "vrf $id";
- foreach my $rule (@$vrf) {
- push @config, " $rule";
-
- }
- push @config, "exit-vrf";
- }
-
- push @{$final_config}, @config;
-}
-
-sub generate_frr_simple_list {
- my ($final_config, $rules) = @_;
-
- return if !$rules;
-
- my @config = ();
- push @{$final_config}, "!";
- foreach my $rule (sort @$rules) {
- push @{$final_config}, $rule;
- }
-}
-
-sub generate_frr_list {
- my ($final_config, $lists, $type) = @_;
-
- my $config = [];
-
- for my $id (sort keys %$lists) {
- my $list = $lists->{$id};
-
- for my $seq (sort keys %$list) {
- my $rule = $list->{$seq};
- push @$config, "$type $id seq $seq $rule";
- }
- }
-
- if (@$config > 0) {
- push @{$final_config}, "!", @$config;
- }
-}
-
-sub generate_frr_interfaces {
- my ($final_config, $interfaces) = @_;
-
- foreach my $k (sort keys %$interfaces) {
- my $iface = $interfaces->{$k};
- push @{$final_config}, "!";
- push @{$final_config}, "interface $k";
- foreach my $rule (sort @$iface) {
- push @{$final_config}, " $rule";
- }
- }
-}
-
-sub generate_frr_routemap {
- my ($final_config, $routemaps) = @_;
-
- foreach my $id (sort keys %$routemaps) {
-
- my $routemap = $routemaps->{$id};
- my $order = 0;
- foreach my $seq (@$routemap) {
- $order++;
- next if !defined($seq->{action});
- my @config = ();
- push @config, "!";
- push @config, "route-map $id $seq->{action} $order";
- my $rule = $seq->{rule};
- push @config, map { " $_" } @$rule;
- push @{$final_config}, @config;
- push @{$final_config}, "exit";
- }
- }
-}
-
1;
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH pve-network v2 5/9] tests: rearrange some statements in the frr config
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (12 preceding siblings ...)
2026-03-02 12:55 ` [PATCH pve-network v2 4/9] sdn: write structured frr config that can be rendered using templates Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH pve-network v2 6/9] sdn: adjust frr.conf.local merging to rust template types Gabriel Goller
` (4 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
Move some stuff up and down, this makes it easier to generate the config
using templates.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
.../evpn/ebgp_loopback/expected_controller_config | 2 +-
.../zones/evpn/isis/expected_controller_config | 14 ++++++++------
.../evpn/isis_loopback/expected_controller_config | 14 ++++++++------
.../isis_standalone/expected_controller_config | 14 ++++++++------
.../multipath_relax/expected_controller_config | 2 +-
.../openfabric_fabric/expected_controller_config | 12 ++++++------
.../evpn/ospf_fabric/expected_controller_config | 12 ++++++------
7 files changed, 38 insertions(+), 32 deletions(-)
diff --git a/src/test/zones/evpn/ebgp_loopback/expected_controller_config b/src/test/zones/evpn/ebgp_loopback/expected_controller_config
index 38f62d06540c..aedb2daef418 100644
--- a/src/test/zones/evpn/ebgp_loopback/expected_controller_config
+++ b/src/test/zones/evpn/ebgp_loopback/expected_controller_config
@@ -14,6 +14,7 @@ router bgp 65001
no bgp default ipv4-unicast
coalesce-time 1000
no bgp graceful-restart notification
+ bgp disable-ebgp-connected-route-check
neighbor VTEP peer-group
neighbor VTEP remote-as external
neighbor VTEP bfd
@@ -21,7 +22,6 @@ router bgp 65001
neighbor VTEP update-source dummy1
neighbor 192.168.0.2 peer-group VTEP
neighbor 192.168.0.3 peer-group VTEP
- bgp disable-ebgp-connected-route-check
neighbor BGP peer-group
neighbor BGP remote-as external
neighbor BGP bfd
diff --git a/src/test/zones/evpn/isis/expected_controller_config b/src/test/zones/evpn/isis/expected_controller_config
index 2361879e0cc2..af9ba30ac97e 100644
--- a/src/test/zones/evpn/isis/expected_controller_config
+++ b/src/test/zones/evpn/isis/expected_controller_config
@@ -8,12 +8,6 @@ vrf vrf_myzone
vni 1000
exit-vrf
!
-interface eth0
- ip router isis isis1
-!
-interface eth1
- ip router isis isis1
-!
router bgp 65000
bgp router-id 192.168.0.1
no bgp hard-administrative-reset
@@ -47,6 +41,14 @@ router isis isis1
log-adjacency-changes
exit
!
+interface eth0
+ ip router isis isis1
+exit
+!
+interface eth1
+ ip router isis isis1
+exit
+!
route-map MAP_VTEP_IN permit 1
exit
!
diff --git a/src/test/zones/evpn/isis_loopback/expected_controller_config b/src/test/zones/evpn/isis_loopback/expected_controller_config
index f4a6e353377b..0eed628286af 100644
--- a/src/test/zones/evpn/isis_loopback/expected_controller_config
+++ b/src/test/zones/evpn/isis_loopback/expected_controller_config
@@ -8,12 +8,6 @@ vrf vrf_myzone
vni 1000
exit-vrf
!
-interface eth0
- ip router isis isis1
-!
-interface eth1
- ip router isis isis1
-!
router bgp 65000
bgp router-id 10.0.0.1
no bgp hard-administrative-reset
@@ -48,6 +42,14 @@ router isis isis1
log-adjacency-changes
exit
!
+interface eth0
+ ip router isis isis1
+exit
+!
+interface eth1
+ ip router isis isis1
+exit
+!
route-map MAP_VTEP_IN permit 1
exit
!
diff --git a/src/test/zones/evpn/isis_standalone/expected_controller_config b/src/test/zones/evpn/isis_standalone/expected_controller_config
index 2cf4dcbf359a..149e4e1f8b79 100644
--- a/src/test/zones/evpn/isis_standalone/expected_controller_config
+++ b/src/test/zones/evpn/isis_standalone/expected_controller_config
@@ -4,12 +4,6 @@ hostname localhost
log syslog informational
service integrated-vtysh-config
!
-interface eth0
- ip router isis isis1
-!
-interface eth1
- ip router isis isis1
-!
router isis isis1
net 47.0023.0000.0000.0000.0000.0000.0000.1900.0004.00
redistribute ipv4 connected level-1
@@ -17,5 +11,13 @@ router isis isis1
log-adjacency-changes
exit
!
+interface eth0
+ ip router isis isis1
+exit
+!
+interface eth1
+ ip router isis isis1
+exit
+!
line vty
!
diff --git a/src/test/zones/evpn/multipath_relax/expected_controller_config b/src/test/zones/evpn/multipath_relax/expected_controller_config
index 783d05cf24d5..26b70b767782 100644
--- a/src/test/zones/evpn/multipath_relax/expected_controller_config
+++ b/src/test/zones/evpn/multipath_relax/expected_controller_config
@@ -14,12 +14,12 @@ router bgp 65000
no bgp default ipv4-unicast
coalesce-time 1000
no bgp graceful-restart notification
+ bgp bestpath as-path multipath-relax
neighbor VTEP peer-group
neighbor VTEP remote-as 65000
neighbor VTEP bfd
neighbor 192.168.0.2 peer-group VTEP
neighbor 192.168.0.3 peer-group VTEP
- bgp bestpath as-path multipath-relax
neighbor BGP peer-group
neighbor BGP remote-as 65000
neighbor BGP bfd
diff --git a/src/test/zones/evpn/openfabric_fabric/expected_controller_config b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
index f3d82ef242be..e749e279769e 100644
--- a/src/test/zones/evpn/openfabric_fabric/expected_controller_config
+++ b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
@@ -35,12 +35,6 @@ router bgp 65000 vrf vrf_evpn
no bgp graceful-restart notification
exit
!
-route-map MAP_VTEP_IN permit 1
-exit
-!
-route-map MAP_VTEP_OUT permit 1
-exit
-!
router openfabric test
net 49.0001.1720.2000.3001.00
exit
@@ -62,6 +56,12 @@ exit
!
access-list pve_openfabric_test_ips permit 172.20.3.0/24
!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+!
route-map pve_openfabric permit 100
match ip address pve_openfabric_test_ips
set src 172.20.3.1
diff --git a/src/test/zones/evpn/ospf_fabric/expected_controller_config b/src/test/zones/evpn/ospf_fabric/expected_controller_config
index 3e41a56eeb9d..b5a1b925213c 100644
--- a/src/test/zones/evpn/ospf_fabric/expected_controller_config
+++ b/src/test/zones/evpn/ospf_fabric/expected_controller_config
@@ -35,12 +35,6 @@ router bgp 65000 vrf vrf_evpn
no bgp graceful-restart notification
exit
!
-route-map MAP_VTEP_IN permit 1
-exit
-!
-route-map MAP_VTEP_OUT permit 1
-exit
-!
router ospf
ospf router-id 172.20.30.1
exit
@@ -56,6 +50,12 @@ exit
!
access-list pve_ospf_test_ips permit 172.20.30.0/24
!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+!
route-map pve_ospf permit 100
match ip address pve_ospf_test_ips
set src 172.20.30.1
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH pve-network v2 6/9] sdn: adjust frr.conf.local merging to rust template types
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (13 preceding siblings ...)
2026-03-02 12:55 ` [PATCH pve-network v2 5/9] tests: rearrange some statements in the frr config Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-03 15:19 ` Stefan Hanreich
2026-03-02 12:55 ` [PATCH pve-network v2 7/9] api: add dry-run endpoint for sdn apply to preview changes Gabriel Goller
` (3 subsequent siblings)
18 siblings, 1 reply; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
The frr object in perl which stores the whole frr config is now also
modeled in rust, so it changed a bit. Adjust the frr.conf.local merging
code so that the frr.conf.local is still merged correctly. This makes
use of the `custom_frr_config` properties scattered in many rust types.
So if we encounter a line in frr.conf.local that we need to merge, we
just throw it into this string vec and render it as-is.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
src/PVE/Network/SDN/Frr.pm | 202 ++++++++++++++++++++++++++++++-------
1 file changed, 168 insertions(+), 34 deletions(-)
diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
index b572f4536004..af3074017ddf 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -271,70 +271,204 @@ sub append_local_config {
return if !$local_config;
my $section = \$frr_config->{""};
- my $router = undef;
+ my $isis_router_name = undef;
+ my $bgp_router_asn = undef;
+ my $bgp_router_vrf = undef;
my $routemap = undef;
- my $routemap_config = ();
- my $routemap_action = undef;
+ my $interface = undef;
+ my $vrf = undef;
+ my $new_block = 0;
+ my $new_af_block = 0;
- while ($local_config =~ /^\s*(.+?)\s*$/gm) {
+ while ($local_config =~ /^(.+?)\s*$/gm) {
my $line = $1;
- $line =~ s/^\s+|\s+$//g;
-
- if ($line =~ m/^router (.+)$/) {
- $router = $1;
- $section = \$frr_config->{'frr'}->{'router'}->{$router}->{""};
+ $line =~ s/\s+$//g;
+
+ if ($line =~ m/^router isis (.+)$/) {
+ $isis_router_name = $1;
+ if (defined $frr_config->{'frr'}->{'isis'}->{'router'}->{$isis_router_name}) {
+ $section =
+ \($frr_config->{'frr'}->{'isis'}->{'router'}->{$isis_router_name}
+ ->{'custom_frr_config'} //= []);
+ } else {
+ $new_block = 1;
+ push(
+ $frr_config->{'frr'}->{'custom_frr_config'}->@*,
+ "router isis $isis_router_name",
+ );
+ $section = \$frr_config->{'frr'}->{'custom_frr_config'};
+ }
+ next;
+ } elsif ($line =~ m/^router bgp (\S+)(?: vrf (.+))?$/) {
+ $bgp_router_asn = $1;
+ $bgp_router_vrf = $2 // 'default';
+
+ my $config_line =
+ defined($2)
+ ? "router bgp $bgp_router_asn vrf $bgp_router_vrf"
+ : "router bgp $bgp_router_asn";
+
+ if (
+ defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
+ and $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}->{'asn'}
+ eq $bgp_router_asn
+ ) {
+ $section =
+ \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
+ ->{'custom_frr_config'} //= []);
+ } else {
+ $new_block = 1;
+ push(
+ $frr_config->{'frr'}->{'custom_frr_config'}->@*, $config_line,
+ );
+ $section = \$frr_config->{'frr'}->{'custom_frr_config'};
+ }
next;
} elsif ($line =~ m/^vrf (.+)$/) {
- $section = \$frr_config->{'frr'}->{'vrf'}->{$1};
+ $vrf = $1;
+ if (defined $frr_config->{'frr'}->{'bgp'}->{'vrfs'}->{$vrf}) {
+ $section = \$frr_config->{'frr'}->{'bgp'}->{'vrfs'}->{$vrf}->{'custom_frr_config'};
+ } else {
+ $new_block = 1;
+ push($frr_config->{'frr'}->{'custom_frr_config'}->@*, "vrf $vrf");
+ $section = \$frr_config->{'frr'}->{'custom_frr_config'};
+ }
next;
} elsif ($line =~ m/^interface (.+)$/) {
- $section = \$frr_config->{'frr_interfaces'}->{$1};
+ $interface = $1;
+ if (defined $frr_config->{'frr'}->{'isis'}->{'interfaces'}->{$interface}) {
+ $section = \($frr_config->{'frr'}->{'isis'}->{'interfaces'}->{$interface}
+ ->{'custom_frr_config'} //= []);
+ } else {
+ $new_block = 1;
+ push(
+ $frr_config->{'frr'}->{'custom_frr_config'}->@*, "interface $interface",
+ );
+ $section = \$frr_config->{'frr'}->{'custom_frr_config'};
+ }
next;
} elsif ($line =~ m/^bgp community-list (.+)$/) {
- push(@{ $frr_config->{'frr_bgp_community_list'} }, $line);
+ push(@{ $frr_config->{'frr'}->{'custom_frr_config'} }, $line);
next;
} elsif ($line =~ m/address-family (.+)$/) {
- $section = \$frr_config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1};
+ # convert the address family from frr (e.g. l2vpn evpn) into the rust property (e.g. l2vpn_evpn)
+ my $address_family_unchanged = $1;
+ my $address_family = $1 =~ s/ /_/gr;
+
+ if (
+ defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
+ and $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}->{'asn'}
+ eq $bgp_router_asn
+ ) {
+ if (
+ defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
+ ->{'address_families'}->{$address_family}
+ ) {
+ $section =
+ \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
+ ->{'address_families'}->{$address_family}->{custom_frr_config} //= []);
+ } else {
+ $new_af_block = 1;
+ push(
+ $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
+ ->{'custom_frr_config'}->@*,
+ " address-family $address_family_unchanged",
+ );
+ $section = \$frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
+ ->{'custom_frr_config'};
+ }
+ } else {
+ $new_af_block = 1;
+ push(
+ $frr_config->{'frr'}->{'custom_frr_config'}->@*,
+ " address-family $address_family_unchanged",
+ );
+ $section = \$frr_config->{'frr'}->{'custom_frr_config'};
+ }
next;
} elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
$routemap = $1;
- $routemap_config = ();
- $routemap_action = $2;
- $section = \$frr_config->{'frr_routemap'}->{$routemap};
+ my $routemap_action = $2;
+ my $seq_number = $3;
+ if (defined $frr_config->{'frr'}->{'routemaps'}->{$routemap}) {
+ my $index = 0;
+ foreach my $single_routemap ($frr_config->{'frr'}->{'routemaps'}->{$routemap}->@*) {
+ if (
+ $single_routemap->{'seq'} == $seq_number
+ && $single_routemap->{'action'} eq $routemap_action
+ ) {
+ last;
+ }
+ $index++;
+ }
+ if ($index < scalar @{ $frr_config->{'frr'}->{'routemaps'}->{$routemap} }) {
+ $section = \($frr_config->{'frr'}->{'routemaps'}->{$routemap}->[$index]
+ ->{'custom_frr_config'} //= []);
+ } else {
+ $new_block = 1;
+ push(
+ $frr_config->{'frr'}->{'custom_frr_config'}->@*,
+ "route-map $routemap $routemap_action $seq_number",
+ );
+ $section = \$frr_config->{'frr'}->{'custom_frr_config'};
+ }
+ } else {
+ $new_block = 1;
+ push(
+ $frr_config->{'frr'}->{'custom_frr_config'}->@*,
+ "route-map $routemap $routemap_action $seq_number",
+ );
+ $section = \$frr_config->{'frr'}->{'custom_frr_config'};
+ }
next;
} elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) {
- $frr_config->{'frr_access_list'}->{$1}->{$2} = $3;
+ push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
next;
} elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) {
- $frr_config->{'frr_prefix_list'}->{$1}->{$2} = $3;
+ push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
next;
} elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) {
- $frr_config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3;
+ push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
next;
- } elsif ($line =~ m/^exit-address-family$/) {
+ } elsif ($line =~ m/exit-address-family$/) {
+ if ($new_af_block) {
+ push(@{$$section}, $line);
+ $section = \$frr_config->{'frr'}->{'bgp'}->{'custom_frr_config'};
+ } else {
+ $section =
+ \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
+ ->{'custom_frr_config'} //= []);
+ }
+ $new_af_block = 0;
next;
- } elsif ($line =~ m/^exit$/) {
- if ($router) {
- $section = \$frr_config->{''};
- $router = undef;
- } elsif ($routemap) {
- push(@{$$section}, { rule => $routemap_config, action => $routemap_action });
- $section = \$frr_config->{''};
+ } elsif ($line =~ m/^exit/) {
+ if ($bgp_router_vrf || $vrf || $interface || $routemap || $isis_router_name) {
+ # this means we just added a new router/vrf/interface/routemap
+ if ($new_block) {
+ push(@{$$section}, $line);
+ push(@{$$section}, "!");
+ }
+ $section = \$frr_config->{'frr'}->{'custom_frr_config'};
+ # we can't stack these, so exit out of all of them (technically we can have a vrf inside of a router bgp block, but we don't support that)
+ $isis_router_name = undef;
+ $bgp_router_vrf = undef;
+ $bgp_router_asn = undef;
+ $vrf = undef;
+ $interface = undef;
$routemap = undef;
- $routemap_action = undef;
- $routemap_config = ();
+ } else {
+ $section = \$frr_config->{'frr'}->{'custom_frr_config'};
+ push(@{$$section}, $line);
+ push(@{$$section}, "!");
}
+ $new_block = 0;
next;
} elsif ($line =~ m/!/) {
next;
}
next if !$section;
- if ($routemap) {
- push(@{$routemap_config}, $line);
- } else {
- push(@{$$section}, $line);
- }
+ push(@{$$section}, $line);
}
}
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* Re: [PATCH pve-network v2 6/9] sdn: adjust frr.conf.local merging to rust template types
2026-03-02 12:55 ` [PATCH pve-network v2 6/9] sdn: adjust frr.conf.local merging to rust template types Gabriel Goller
@ 2026-03-03 15:19 ` Stefan Hanreich
2026-03-04 14:37 ` Gabriel Goller
0 siblings, 1 reply; 22+ messages in thread
From: Stefan Hanreich @ 2026-03-03 15:19 UTC (permalink / raw)
To: Gabriel Goller, pve-devel
On 3/2/26 1:56 PM, Gabriel Goller wrote:
> The frr object in perl which stores the whole frr config is now also
> modeled in rust, so it changed a bit. Adjust the frr.conf.local merging
> code so that the frr.conf.local is still merged correctly. This makes
> use of the `custom_frr_config` properties scattered in many rust types.
> So if we encounter a line in frr.conf.local that we need to merge, we
> just throw it into this string vec and render it as-is.
>
> Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
> src/PVE/Network/SDN/Frr.pm | 202 ++++++++++++++++++++++++++++++-------
> 1 file changed, 168 insertions(+), 34 deletions(-)
>
> diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
> index b572f4536004..af3074017ddf 100644
> --- a/src/PVE/Network/SDN/Frr.pm
> +++ b/src/PVE/Network/SDN/Frr.pm
> @@ -271,70 +271,204 @@ sub append_local_config {
> return if !$local_config;
>
> my $section = \$frr_config->{""};
> - my $router = undef;
> + my $isis_router_name = undef;
> + my $bgp_router_asn = undef;
> + my $bgp_router_vrf = undef;
> my $routemap = undef;
> - my $routemap_config = ();
> - my $routemap_action = undef;
> + my $interface = undef;
> + my $vrf = undef;
> + my $new_block = 0;
> + my $new_af_block = 0;
>
> - while ($local_config =~ /^\s*(.+?)\s*$/gm) {
> + while ($local_config =~ /^(.+?)\s*$/gm) {
> my $line = $1;
> - $line =~ s/^\s+|\s+$//g;
> -
> - if ($line =~ m/^router (.+)$/) {
> - $router = $1;
> - $section = \$frr_config->{'frr'}->{'router'}->{$router}->{""};
after this change merging now only works for isis and bgp routers? Did a
quick check with other routing protocols in frr.conf.local and the
routers seem to be missing.
> + $line =~ s/\s+$//g;
> +
> + if ($line =~ m/^router isis (.+)$/) {
> + $isis_router_name = $1;
> + if (defined $frr_config->{'frr'}->{'isis'}->{'router'}->{$isis_router_name}) {
calls to defined should use parentheses (several occurences below)
> + $section =
> + \($frr_config->{'frr'}->{'isis'}->{'router'}->{$isis_router_name}
> + ->{'custom_frr_config'} //= []);
> + } else {
> + $new_block = 1;
> + push(
> + $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> + "router isis $isis_router_name",
> + );
> + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> + }
> + next;
> + } elsif ($line =~ m/^router bgp (\S+)(?: vrf (.+))?$/) {
> + $bgp_router_asn = $1;
> + $bgp_router_vrf = $2 // 'default';
> +
> + my $config_line =
> + defined($2)
> + ? "router bgp $bgp_router_asn vrf $bgp_router_vrf"
> + : "router bgp $bgp_router_asn";
> +
this is only required in the else branch?
> + if (
> + defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> + and $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}->{'asn'}
> + eq $bgp_router_asn
> + ) {
> + $section =
> + \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> + ->{'custom_frr_config'} //= []);
> + } else {
> + $new_block = 1;
> + push(
> + $frr_config->{'frr'}->{'custom_frr_config'}->@*, $config_line,
> + );
> + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> + }
> next;
> } elsif ($line =~ m/^vrf (.+)$/) {
> - $section = \$frr_config->{'frr'}->{'vrf'}->{$1};
> + $vrf = $1;
> + if (defined $frr_config->{'frr'}->{'bgp'}->{'vrfs'}->{$vrf}) {
> + $section = \$frr_config->{'frr'}->{'bgp'}->{'vrfs'}->{$vrf}->{'custom_frr_config'};
> + } else {
> + $new_block = 1;
> + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, "vrf $vrf");
> + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> + }
> next;
> } elsif ($line =~ m/^interface (.+)$/) {
> - $section = \$frr_config->{'frr_interfaces'}->{$1};
> + $interface = $1;
> + if (defined $frr_config->{'frr'}->{'isis'}->{'interfaces'}->{$interface}) {
trying to override an existing IS-IS interface doesn't work for me, e.g.
with the following IS-IS controller:
isis: isisberserker
isis-domain 1
isis-ifaces ens19,ens20
isis-net 49.0000.1234.0000.00
node berserker
and the following frr.conf.local:
interface ens20
isis circuit-type level-2-only
exit
!
I get an deserialization error:
error: invalid type: Option value, expected a sequence
This seems to be because the generated $frr_config has
'custom_frr_config' => undef
in its top-level.
> + $section = \($frr_config->{'frr'}->{'isis'}->{'interfaces'}->{$interface}
> + ->{'custom_frr_config'} //= []);
> + } else {
> + $new_block = 1;
> + push(
> + $frr_config->{'frr'}->{'custom_frr_config'}->@*, "interface $interface",
> + );
> + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> + }
> next;
> } elsif ($line =~ m/^bgp community-list (.+)$/) {
> - push(@{ $frr_config->{'frr_bgp_community_list'} }, $line);
> + push(@{ $frr_config->{'frr'}->{'custom_frr_config'} }, $line);
> next;
> } elsif ($line =~ m/address-family (.+)$/) {
> - $section = \$frr_config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1};
> + # convert the address family from frr (e.g. l2vpn evpn) into the rust property (e.g. l2vpn_evpn)
> + my $address_family_unchanged = $1;
> + my $address_family = $1 =~ s/ /_/gr;
> +
> + if (
> + defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> + and $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}->{'asn'}
> + eq $bgp_router_asn
> + ) {
> + if (
> + defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> + ->{'address_families'}->{$address_family}
> + ) {
> + $section =
> + \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> + ->{'address_families'}->{$address_family}->{custom_frr_config} //= []);
> + } else {
> + $new_af_block = 1;
> + push(
> + $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> + ->{'custom_frr_config'}->@*,
> + " address-family $address_family_unchanged",
> + );
> + $section = \$frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> + ->{'custom_frr_config'};
> + }
> + } else {
> + $new_af_block = 1;
> + push(
> + $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> + " address-family $address_family_unchanged",
> + );
> + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> + }
> next;
> } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
> $routemap = $1;
> - $routemap_config = ();
> - $routemap_action = $2;
> - $section = \$frr_config->{'frr_routemap'}->{$routemap};
> + my $routemap_action = $2;
> + my $seq_number = $3;
> + if (defined $frr_config->{'frr'}->{'routemaps'}->{$routemap}) {
> + my $index = 0;
> + foreach my $single_routemap ($frr_config->{'frr'}->{'routemaps'}->{$routemap}->@*) {
for is preferred over foreach, see:
https://pve.proxmox.com/wiki/Perl_Style_Guide#Perl_syntax_choices
> + if (
> + $single_routemap->{'seq'} == $seq_number
> + && $single_routemap->{'action'} eq $routemap_action
> + ) {
> + last;
> + }
> + $index++;
> + }
> + if ($index < scalar @{ $frr_config->{'frr'}->{'routemaps'}->{$routemap} }) {
> + $section = \($frr_config->{'frr'}->{'routemaps'}->{$routemap}->[$index]
> + ->{'custom_frr_config'} //= []);
> + } else {
> + $new_block = 1;
> + push(
> + $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> + "route-map $routemap $routemap_action $seq_number",
> + );
> + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> + }
> + } else {
> + $new_block = 1;
> + push(
> + $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> + "route-map $routemap $routemap_action $seq_number",
> + );
> + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> + }
> next;
adding entries for an existing route-map (i.e. MAP_VTEP_IN) like so:
route-map MAP_VTEP_IN deny 1
exit
!
route-map MAP_VTEP_IN permit 1
exit
!
route-map MAP_VTEP_IN permit 1
match community cm-prefmod-400
set local-preference 400
exit
!
route-map MAP_VTEP_IN permit 1
match ip address 10
set local-preference 200
exit
!
merges the route-map configuration with this patch, if the verdict is
the same:
route-map MAP_VTEP_IN permit 1
match community cm-prefmod-400
set local-preference 400
match ip address 10
set local-preference 200
exit
!
the section with the same seq nr, but different verdict gets
additionally added:
route-map MAP_VTEP_IN deny 1
exit
!
------------------------------------------
with the old merging logic, they were added as separate sections, with
increasing numbers:
route-map MAP_VTEP_IN permit 1
exit
!
route-map MAP_VTEP_IN deny 2
exit
!
route-map MAP_VTEP_IN permit 3
exit
!
route-map MAP_VTEP_IN permit 4
match community cm-prefmod-400
set local-preference 400
exit
!
route-map MAP_VTEP_IN permit 5
match ip address 10
set local-preference 200
exit
!
Is this intentional? Imo this is quite the breaking change, particularly
because route-maps are probably used relatively often in the frr.conf.local?
> } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) {
> - $frr_config->{'frr_access_list'}->{$1}->{$2} = $3;
> + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
> next;
> } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) {
> - $frr_config->{'frr_prefix_list'}->{$1}->{$2} = $3;
> + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
> next;
> } elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) {
> - $frr_config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3;
> + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
> next;
> - } elsif ($line =~ m/^exit-address-family$/) {
> + } elsif ($line =~ m/exit-address-family$/) {
> + if ($new_af_block) {
> + push(@{$$section}, $line);
> + $section = \$frr_config->{'frr'}->{'bgp'}->{'custom_frr_config'};
> + } else {
> + $section =
> + \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> + ->{'custom_frr_config'} //= []);
> + }
> + $new_af_block = 0;
> next;
> - } elsif ($line =~ m/^exit$/) {
> - if ($router) {
> - $section = \$frr_config->{''};
> - $router = undef;
> - } elsif ($routemap) {
> - push(@{$$section}, { rule => $routemap_config, action => $routemap_action });
> - $section = \$frr_config->{''};
> + } elsif ($line =~ m/^exit/) {
> + if ($bgp_router_vrf || $vrf || $interface || $routemap || $isis_router_name) {
> + # this means we just added a new router/vrf/interface/routemap
> + if ($new_block) {
> + push(@{$$section}, $line);
> + push(@{$$section}, "!");
> + }
> + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> + # we can't stack these, so exit out of all of them (technically we can have a vrf inside of a router bgp block, but we don't support that)
> + $isis_router_name = undef;
> + $bgp_router_vrf = undef;
> + $bgp_router_asn = undef;
> + $vrf = undef;
> + $interface = undef;
> $routemap = undef;
> - $routemap_action = undef;
> - $routemap_config = ();
> + } else {
> + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> + push(@{$$section}, $line);
> + push(@{$$section}, "!");
> }
> + $new_block = 0;
> next;
> } elsif ($line =~ m/!/) {
> next;
> }
>
> next if !$section;
> - if ($routemap) {
> - push(@{$routemap_config}, $line);
> - } else {
> - push(@{$$section}, $line);
> - }
> + push(@{$$section}, $line);
> }
> }
>
^ permalink raw reply [flat|nested] 22+ messages in thread* Re: [PATCH pve-network v2 6/9] sdn: adjust frr.conf.local merging to rust template types
2026-03-03 15:19 ` Stefan Hanreich
@ 2026-03-04 14:37 ` Gabriel Goller
0 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-04 14:37 UTC (permalink / raw)
To: Stefan Hanreich; +Cc: pve-devel
On 03.03.2026 16:19, Stefan Hanreich wrote:
> On 3/2/26 1:56 PM, Gabriel Goller wrote:
> > The frr object in perl which stores the whole frr config is now also
> > modeled in rust, so it changed a bit. Adjust the frr.conf.local merging
> > code so that the frr.conf.local is still merged correctly. This makes
> > use of the `custom_frr_config` properties scattered in many rust types.
> > So if we encounter a line in frr.conf.local that we need to merge, we
> > just throw it into this string vec and render it as-is.
> >
> > Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
> > Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> > ---
> > src/PVE/Network/SDN/Frr.pm | 202 ++++++++++++++++++++++++++++++-------
> > 1 file changed, 168 insertions(+), 34 deletions(-)
> >
> > diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
> > index b572f4536004..af3074017ddf 100644
> > --- a/src/PVE/Network/SDN/Frr.pm
> > +++ b/src/PVE/Network/SDN/Frr.pm
> > @@ -271,70 +271,204 @@ sub append_local_config {
> > return if !$local_config;
> >
> > my $section = \$frr_config->{""};
> > - my $router = undef;
> > + my $isis_router_name = undef;
> > + my $bgp_router_asn = undef;
> > + my $bgp_router_vrf = undef;
> > my $routemap = undef;
> > - my $routemap_config = ();
> > - my $routemap_action = undef;
> > + my $interface = undef;
> > + my $vrf = undef;
> > + my $new_block = 0;
> > + my $new_af_block = 0;
> >
> > - while ($local_config =~ /^\s*(.+?)\s*$/gm) {
> > + while ($local_config =~ /^(.+?)\s*$/gm) {
> > my $line = $1;
> > - $line =~ s/^\s+|\s+$//g;
> > -
> > - if ($line =~ m/^router (.+)$/) {
> > - $router = $1;
> > - $section = \$frr_config->{'frr'}->{'router'}->{$router}->{""};
>
> after this change merging now only works for isis and bgp routers? Did a
> quick check with other routing protocols in frr.conf.local and the
> routers seem to be missing.
Good catch, fixed this!
> > + $line =~ s/\s+$//g;
> > +
> > + if ($line =~ m/^router isis (.+)$/) {
> > + $isis_router_name = $1;
> > + if (defined $frr_config->{'frr'}->{'isis'}->{'router'}->{$isis_router_name}) {
>
> calls to defined should use parentheses (several occurences below)
done!
> > + $section =
> > + \($frr_config->{'frr'}->{'isis'}->{'router'}->{$isis_router_name}
> > + ->{'custom_frr_config'} //= []);
> > + } else {
> > + $new_block = 1;
> > + push(
> > + $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> > + "router isis $isis_router_name",
> > + );
> > + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> > + }
> > + next;
> > + } elsif ($line =~ m/^router bgp (\S+)(?: vrf (.+))?$/) {
> > + $bgp_router_asn = $1;
> > + $bgp_router_vrf = $2 // 'default';
> > +
> > + my $config_line =
> > + defined($2)
> > + ? "router bgp $bgp_router_asn vrf $bgp_router_vrf"
> > + : "router bgp $bgp_router_asn";
> > +
>
> this is only required in the else branch?
moved it down.
> > + if (
> > + defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> > + and $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}->{'asn'}
> > + eq $bgp_router_asn
> > + ) {
> > + $section =
> > + \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> > + ->{'custom_frr_config'} //= []);
> > + } else {
> > + $new_block = 1;
> > + push(
> > + $frr_config->{'frr'}->{'custom_frr_config'}->@*, $config_line,
> > + );
> > + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> > + }
> > next;
> > } elsif ($line =~ m/^vrf (.+)$/) {
> > - $section = \$frr_config->{'frr'}->{'vrf'}->{$1};
> > + $vrf = $1;
> > + if (defined $frr_config->{'frr'}->{'bgp'}->{'vrfs'}->{$vrf}) {
> > + $section = \$frr_config->{'frr'}->{'bgp'}->{'vrfs'}->{$vrf}->{'custom_frr_config'};
> > + } else {
> > + $new_block = 1;
> > + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, "vrf $vrf");
> > + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> > + }
> > next;
> > } elsif ($line =~ m/^interface (.+)$/) {
> > - $section = \$frr_config->{'frr_interfaces'}->{$1};
> > + $interface = $1;
> > + if (defined $frr_config->{'frr'}->{'isis'}->{'interfaces'}->{$interface}) {
>
> trying to override an existing IS-IS interface doesn't work for me, e.g.
> with the following IS-IS controller:
>
> isis: isisberserker
> isis-domain 1
> isis-ifaces ens19,ens20
> isis-net 49.0000.1234.0000.00
> node berserker
>
> and the following frr.conf.local:
>
> interface ens20
> isis circuit-type level-2-only
> exit
> !
>
> I get an deserialization error:
>
> error: invalid type: Option value, expected a sequence
>
>
> This seems to be because the generated $frr_config has
>
> 'custom_frr_config' => undef
>
> in its top-level.
fixed this.
> > + $section = \($frr_config->{'frr'}->{'isis'}->{'interfaces'}->{$interface}
> > + ->{'custom_frr_config'} //= []);
> > + } else {
> > + $new_block = 1;
> > + push(
> > + $frr_config->{'frr'}->{'custom_frr_config'}->@*, "interface $interface",
> > + );
> > + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> > + }
> > next;
> > } elsif ($line =~ m/^bgp community-list (.+)$/) {
> > - push(@{ $frr_config->{'frr_bgp_community_list'} }, $line);
> > + push(@{ $frr_config->{'frr'}->{'custom_frr_config'} }, $line);
> > next;
> > } elsif ($line =~ m/address-family (.+)$/) {
> > - $section = \$frr_config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1};
> > + # convert the address family from frr (e.g. l2vpn evpn) into the rust property (e.g. l2vpn_evpn)
> > + my $address_family_unchanged = $1;
> > + my $address_family = $1 =~ s/ /_/gr;
> > +
> > + if (
> > + defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> > + and $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}->{'asn'}
> > + eq $bgp_router_asn
> > + ) {
> > + if (
> > + defined $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> > + ->{'address_families'}->{$address_family}
> > + ) {
> > + $section =
> > + \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> > + ->{'address_families'}->{$address_family}->{custom_frr_config} //= []);
> > + } else {
> > + $new_af_block = 1;
> > + push(
> > + $frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> > + ->{'custom_frr_config'}->@*,
> > + " address-family $address_family_unchanged",
> > + );
> > + $section = \$frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> > + ->{'custom_frr_config'};
> > + }
> > + } else {
> > + $new_af_block = 1;
> > + push(
> > + $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> > + " address-family $address_family_unchanged",
> > + );
> > + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> > + }
> > next;
> > } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
> > $routemap = $1;
> > - $routemap_config = ();
> > - $routemap_action = $2;
> > - $section = \$frr_config->{'frr_routemap'}->{$routemap};
> > + my $routemap_action = $2;
> > + my $seq_number = $3;
> > + if (defined $frr_config->{'frr'}->{'routemaps'}->{$routemap}) {
> > + my $index = 0;
> > + foreach my $single_routemap ($frr_config->{'frr'}->{'routemaps'}->{$routemap}->@*) {
>
> for is preferred over foreach, see:
> https://pve.proxmox.com/wiki/Perl_Style_Guide#Perl_syntax_choices
done.
> > + if (
> > + $single_routemap->{'seq'} == $seq_number
> > + && $single_routemap->{'action'} eq $routemap_action
> > + ) {
> > + last;
> > + }
> > + $index++;
> > + }
> > + if ($index < scalar @{ $frr_config->{'frr'}->{'routemaps'}->{$routemap} }) {
> > + $section = \($frr_config->{'frr'}->{'routemaps'}->{$routemap}->[$index]
> > + ->{'custom_frr_config'} //= []);
> > + } else {
> > + $new_block = 1;
> > + push(
> > + $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> > + "route-map $routemap $routemap_action $seq_number",
> > + );
> > + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> > + }
> > + } else {
> > + $new_block = 1;
> > + push(
> > + $frr_config->{'frr'}->{'custom_frr_config'}->@*,
> > + "route-map $routemap $routemap_action $seq_number",
> > + );
> > + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> > + }
> > next;
>
> adding entries for an existing route-map (i.e. MAP_VTEP_IN) like so:
>
> route-map MAP_VTEP_IN deny 1
> exit
> !
> route-map MAP_VTEP_IN permit 1
> exit
> !
> route-map MAP_VTEP_IN permit 1
> match community cm-prefmod-400
> set local-preference 400
> exit
> !
> route-map MAP_VTEP_IN permit 1
> match ip address 10
> set local-preference 200
> exit
> !
>
> merges the route-map configuration with this patch, if the verdict is
> the same:
>
> route-map MAP_VTEP_IN permit 1
> match community cm-prefmod-400
> set local-preference 400
> match ip address 10
> set local-preference 200
> exit
> !
>
> the section with the same seq nr, but different verdict gets
> additionally added:
>
> route-map MAP_VTEP_IN deny 1
> exit
> !
>
> ------------------------------------------
>
> with the old merging logic, they were added as separate sections, with
> increasing numbers:
>
> route-map MAP_VTEP_IN permit 1
> exit
> !
> route-map MAP_VTEP_IN deny 2
> exit
> !
> route-map MAP_VTEP_IN permit 3
> exit
> !
> route-map MAP_VTEP_IN permit 4
> match community cm-prefmod-400
> set local-preference 400
> exit
> !
> route-map MAP_VTEP_IN permit 5
> match ip address 10
> set local-preference 200
> exit
> !
>
> Is this intentional? Imo this is quite the breaking change, particularly
> because route-maps are probably used relatively often in the frr.conf.local?
fixed this as well I hope.
> > } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) {
> > - $frr_config->{'frr_access_list'}->{$1}->{$2} = $3;
> > + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
> > next;
> > } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) {
> > - $frr_config->{'frr_prefix_list'}->{$1}->{$2} = $3;
> > + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
> > next;
> > } elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) {
> > - $frr_config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3;
> > + push($frr_config->{'frr'}->{'custom_frr_config'}->@*, $line);
> > next;
> > - } elsif ($line =~ m/^exit-address-family$/) {
> > + } elsif ($line =~ m/exit-address-family$/) {
> > + if ($new_af_block) {
> > + push(@{$$section}, $line);
> > + $section = \$frr_config->{'frr'}->{'bgp'}->{'custom_frr_config'};
> > + } else {
> > + $section =
> > + \($frr_config->{'frr'}->{'bgp'}->{'vrf_router'}->{$bgp_router_vrf}
> > + ->{'custom_frr_config'} //= []);
> > + }
> > + $new_af_block = 0;
> > next;
> > - } elsif ($line =~ m/^exit$/) {
> > - if ($router) {
> > - $section = \$frr_config->{''};
> > - $router = undef;
> > - } elsif ($routemap) {
> > - push(@{$$section}, { rule => $routemap_config, action => $routemap_action });
> > - $section = \$frr_config->{''};
> > + } elsif ($line =~ m/^exit/) {
> > + if ($bgp_router_vrf || $vrf || $interface || $routemap || $isis_router_name) {
> > + # this means we just added a new router/vrf/interface/routemap
> > + if ($new_block) {
> > + push(@{$$section}, $line);
> > + push(@{$$section}, "!");
> > + }
> > + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> > + # we can't stack these, so exit out of all of them (technically we can have a vrf inside of a router bgp block, but we don't support that)
> > + $isis_router_name = undef;
> > + $bgp_router_vrf = undef;
> > + $bgp_router_asn = undef;
> > + $vrf = undef;
> > + $interface = undef;
> > $routemap = undef;
> > - $routemap_action = undef;
> > - $routemap_config = ();
> > + } else {
> > + $section = \$frr_config->{'frr'}->{'custom_frr_config'};
> > + push(@{$$section}, $line);
> > + push(@{$$section}, "!");
> > }
> > + $new_block = 0;
> > next;
> > } elsif ($line =~ m/!/) {
> > next;
> > }
> >
> > next if !$section;
> > - if ($routemap) {
> > - push(@{$routemap_config}, $line);
> > - } else {
> > - push(@{$$section}, $line);
> > - }
> > + push(@{$$section}, $line);
> > }
> > }
> >
Will send a new version soon!
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH pve-network v2 7/9] api: add dry-run endpoint for sdn apply to preview changes
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (14 preceding siblings ...)
2026-03-02 12:55 ` [PATCH pve-network v2 6/9] sdn: adjust frr.conf.local merging to rust template types Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH pve-network v2 8/9] test: add test for frr.conf.local merging Gabriel Goller
` (2 subsequent siblings)
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
Allows users to see the diff of frr and interfaces configuration files
before applying SDN changes. Previously this was not possible and the
user had to apply and then see what changed. For the ifupdown2 dry-run
to work, pull out the running_config generation to the parent functions.
Also add an option to the compile_running_cfg function so that we can
skip the /etc/network/interfaces.d/sdn file version bump. This means
when nothing has been changed and dry-run is pressed, nothing will be
shown.
Rename a few constants so that they don't clash with local variables.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
src/PVE/API2/Network/SDN.pm | 88 ++++++++++++++++++++++++++++++++
src/PVE/Network/SDN.pm | 36 ++++++++-----
src/PVE/Network/SDN/Fabrics.pm | 10 +++-
src/PVE/Network/SDN/Zones.pm | 3 +-
src/test/debug/generateconfig.pl | 3 +-
src/test/run_test_zones.pl | 2 +-
6 files changed, 124 insertions(+), 18 deletions(-)
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index b35a588d391d..31159c27a3ac 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -3,6 +3,9 @@ package PVE::API2::Network::SDN;
use strict;
use warnings;
+use Encode qw(decode);
+use JSON qw(from_json);
+
use PVE::Cluster qw(cfs_lock_file cfs_read_file cfs_write_file);
use PVE::Exception qw(raise_param_exc);
use PVE::JSONSchema qw(get_standard_option);
@@ -11,6 +14,7 @@ use PVE::RPCEnvironment;
use PVE::SafeSyslog;
use PVE::Tools qw(run_command extract_param);
use PVE::Network::SDN;
+use PVE::File;
use PVE::API2::Network::SDN::Controllers;
use PVE::API2::Network::SDN::Vnets;
@@ -325,4 +329,88 @@ __PACKAGE__->register_method({
},
});
+sub get_diff {
+ my ($filename_one, $filename_two) = @_;
+
+ my $diff = '';
+
+ my $cmd = ['/usr/bin/diff', '-b', '-N', '-u', $filename_one, $filename_two];
+ PVE::Tools::run_command(
+ $cmd,
+ noerr => 1,
+ outfunc => sub {
+ my ($line) = @_;
+ $diff .= decode('UTF-8', $line) . "\n";
+ },
+ );
+
+ $diff = undef if !$diff;
+
+ return $diff;
+}
+
+__PACKAGE__->register_method({
+ name => 'dry-run',
+ path => 'dry-run',
+ method => 'PUT',
+ permissions => {
+ check => ['perm', '/nodes/{node}', ['Sys.Modify']],
+ },
+ description =>
+ "Dry-run the SDN apply action and return the difference between the current configuration and the pending configuration",
+ protected => 1,
+ proxyto => 'node',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ },
+ },
+
+ returns => {
+ type => 'object',
+ properties => {
+ "frr-diff" => {
+ type => 'string',
+ description =>
+ 'The difference between the current and pending FRR configuration.',
+ },
+ "interfaces-diff" => {
+ type => 'string',
+ description =>
+ 'The difference between the current and pending /etc/network/interfaces.d/sdn configuration.',
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $cfg = PVE::Network::SDN::compile_running_cfg();
+
+ my $fabric_cfg = PVE::Network::SDN::Fabrics::config(0);
+ my $frr_cfg = PVE::Network::SDN::generate_frr_raw_config($cfg, $fabric_cfg);
+ my $new_cfg_frr = PVE::Network::SDN::Frr::raw_config_to_string($frr_cfg);
+
+ # compile running config and skip version bump
+ my $running_cfg = PVE::Network::SDN::compile_running_cfg(1);
+
+ my $new_interfaces_cfg =
+ PVE::Network::SDN::generate_raw_etc_network_config($running_cfg);
+
+ my ($frr_tmp_filename, $frr_tmp_fh) = PVE::File::tempfile_contents($new_cfg_frr, 700);
+
+ my ($interfaces_tmp_filename, $interfaces_tmp_fh) =
+ PVE::File::tempfile_contents($new_interfaces_cfg, 700);
+
+ my $return_value = {};
+ $return_value->{"frr-diff"} = get_diff('/etc/frr/frr.conf', $frr_tmp_filename);
+ $return_value->{"interfaces-diff"} =
+ get_diff('/etc/network/interfaces.d/sdn', $interfaces_tmp_filename);
+
+ close($frr_tmp_fh);
+ close($interfaces_tmp_fh);
+ return $return_value;
+ },
+});
+
1;
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index c000bed498ec..09a3e02f171f 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -26,7 +26,7 @@ use PVE::Network::SDN::Dhcp;
use PVE::Network::SDN::Frr;
use PVE::Network::SDN::Fabrics;
-my $running_cfg = "sdn/.running-config";
+my $RUNNING_CFG_FILENAME = "sdn/.running-config";
my $parse_running_cfg = sub {
my ($filename, $raw) = @_;
@@ -49,7 +49,7 @@ my $write_running_cfg = sub {
return $json;
};
-PVE::Cluster::cfs_register_file($running_cfg, $parse_running_cfg, $write_running_cfg);
+PVE::Cluster::cfs_register_file($RUNNING_CFG_FILENAME, $parse_running_cfg, $write_running_cfg);
my $LOCK_TOKEN_FILE = "/etc/pve/sdn/.lock";
@@ -105,7 +105,7 @@ sub status {
}
sub running_config {
- return cfs_read_file($running_cfg);
+ return cfs_read_file($RUNNING_CFG_FILENAME);
}
=head3 running_config_has_frr(\%running_config)
@@ -187,13 +187,17 @@ sub pending_config {
}
-sub commit_config {
+sub compile_running_cfg {
+ my ($skip_version_bump) = @_;
+ $skip_version_bump = $skip_version_bump // 0;
- my $cfg = cfs_read_file($running_cfg);
+ my $cfg = cfs_read_file($RUNNING_CFG_FILENAME);
my $version = $cfg->{version};
if ($version) {
- $version++;
+ if (!$skip_version_bump) {
+ $version++;
+ }
} else {
$version = 1;
}
@@ -219,7 +223,13 @@ sub commit_config {
fabrics => $fabrics,
};
- cfs_write_file($running_cfg, $cfg);
+ return $cfg;
+}
+
+sub commit_config {
+ my $cfg = compile_running_cfg();
+
+ cfs_write_file($RUNNING_CFG_FILENAME, $cfg);
}
sub has_pending_changes {
@@ -342,20 +352,21 @@ sub get_local_vnets {
return $vnets;
}
-=head3 generate_raw_etc_network_config()
+=head3 generate_raw_etc_network_config(\%running_cfg)
Generate the /etc/network/interfaces.d/sdn config file from the Zones
-and Fabrics configuration and return it as a String.
+and Fabrics running configuration passed and return it as a String.
=cut
sub generate_raw_etc_network_config {
+ my ($running_cfg) = @_;
my $raw_config = "";
- my $zone_config = PVE::Network::SDN::Zones::generate_etc_network_config();
+ my $zone_config = PVE::Network::SDN::Zones::generate_etc_network_config($running_cfg);
$raw_config .= $zone_config if $zone_config;
- my $fabric_config = PVE::Network::SDN::Fabrics::generate_etc_network_config();
+ my $fabric_config = PVE::Network::SDN::Fabrics::generate_etc_network_config($running_cfg);
$raw_config .= $fabric_config if $fabric_config;
return $raw_config;
@@ -396,7 +407,8 @@ interfaces files (/etc/network/interfaces.d/sdn).
=cut
sub generate_etc_network_config {
- my $raw_config = PVE::Network::SDN::generate_raw_etc_network_config();
+ my $running_cfg = PVE::Network::SDN::running_config();
+ my $raw_config = PVE::Network::SDN::generate_raw_etc_network_config($running_cfg);
PVE::Network::SDN::write_raw_etc_network_config($raw_config);
}
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index 3ca362a02660..9be7f0215bef 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -102,10 +102,16 @@ sub get_frr_daemon_status {
}
sub generate_etc_network_config {
+ my ($running_cfg) = @_;
+
my $nodename = PVE::INotify::nodename();
- my $fabric_config = PVE::Network::SDN::Fabrics::config(1);
+ # if the config hasn't yet been applied after the introduction of
+ # fabrics then the key does not exist in the running config so we
+ # default to an empty hash
+ my $fabrics_config = $running_cfg->{fabrics}->{ids} // {};
+ my $fabric_object = PVE::RS::SDN::Fabrics->running_config($fabrics_config);
- return $fabric_config->get_interfaces_etc_network_config($nodename);
+ return $fabric_object->get_interfaces_etc_network_config($nodename);
}
sub node_properties {
diff --git a/src/PVE/Network/SDN/Zones.pm b/src/PVE/Network/SDN/Zones.pm
index 4da94580e07d..4c1468cf8ef8 100644
--- a/src/PVE/Network/SDN/Zones.pm
+++ b/src/PVE/Network/SDN/Zones.pm
@@ -107,8 +107,7 @@ sub get_vnets {
}
sub generate_etc_network_config {
-
- my $cfg = PVE::Network::SDN::running_config();
+ my ($cfg) = @_;
my $version = $cfg->{version};
my $vnet_cfg = $cfg->{vnets};
diff --git a/src/test/debug/generateconfig.pl b/src/test/debug/generateconfig.pl
index 250db4368186..99a5d59cba72 100644
--- a/src/test/debug/generateconfig.pl
+++ b/src/test/debug/generateconfig.pl
@@ -9,7 +9,8 @@ use PVE::Network::SDN::Controllers;
use Data::Dumper;
PVE::Network::SDN::commit_config();
-my $network_config = PVE::Network::SDN::Zones::generate_etc_network_config();
+my $running_cfg = PVE::Network::SDN::running_config();
+my $network_config = PVE::Network::SDN::Zones::generate_etc_network_config($running_cfg);
PVE::Network::SDN::Zones::write_etc_network_config($network_config);
print "/etc/network/interfaces.d/sdn\n";
diff --git a/src/test/run_test_zones.pl b/src/test/run_test_zones.pl
index 905b2f42e1dc..2d726c87f423 100755
--- a/src/test/run_test_zones.pl
+++ b/src/test/run_test_zones.pl
@@ -135,7 +135,7 @@ foreach my $test (@tests) {
my $name = $test;
my $expected = read_file("./$test/expected_sdn_interfaces");
- my $result = eval { PVE::Network::SDN::generate_raw_etc_network_config() };
+ my $result = eval { PVE::Network::SDN::generate_raw_etc_network_config($sdn_config) };
if (my $err = $@) {
diag("got unexpected error - $err");
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH pve-network v2 8/9] test: add test for frr.conf.local merging
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (15 preceding siblings ...)
2026-03-02 12:55 ` [PATCH pve-network v2 7/9] api: add dry-run endpoint for sdn apply to preview changes Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH pve-network v2 9/9] test: bgp: add some various integration tests Gabriel Goller
2026-03-02 12:55 ` [PATCH pve-manager v2 1/1] sdn: add dry-run diff view for sdn apply Gabriel Goller
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
Add a test that tests the frr.conf.local merging. This should ensure we
do not run into further regressions. The test also "succeeds" with the
pre-templates version, there are just some whitespace and "!" issues.
Also the route-maps are merged instead of pushed with higher sequence
number. This shouldn't change anything.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
src/test/run_test_zones.pl | 16 ++-
.../expected_controller_config | 117 ++++++++++++++++++
.../frr_local_merge/expected_sdn_interfaces | 53 ++++++++
.../zones/evpn/frr_local_merge/frr.conf.local | 59 +++++++++
.../zones/evpn/frr_local_merge/interfaces | 7 ++
.../zones/evpn/frr_local_merge/sdn_config | 73 +++++++++++
6 files changed, 322 insertions(+), 3 deletions(-)
create mode 100644 src/test/zones/evpn/frr_local_merge/expected_controller_config
create mode 100644 src/test/zones/evpn/frr_local_merge/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/frr_local_merge/frr.conf.local
create mode 100644 src/test/zones/evpn/frr_local_merge/interfaces
create mode 100644 src/test/zones/evpn/frr_local_merge/sdn_config
diff --git a/src/test/run_test_zones.pl b/src/test/run_test_zones.pl
index 2d726c87f423..8986c5c52c9f 100755
--- a/src/test/run_test_zones.pl
+++ b/src/test/run_test_zones.pl
@@ -126,12 +126,22 @@ foreach my $test (@tests) {
reload_controller => sub {
return;
},
- read_local_frr_config => sub {
- return;
- },
);
}
+ # Mock read_local_frr_config in PVE::Network::SDN::Frr to support testing frr.conf.local merging
+ my $frr_local_config;
+ my $frr_local_path = "./$test/frr.conf.local";
+ if (-e $frr_local_path) {
+ $frr_local_config = read_file($frr_local_path);
+ }
+ my $mocked_frr = Test::MockModule->new('PVE::Network::SDN::Frr');
+ $mocked_frr->mock(
+ read_local_frr_config => sub {
+ return $frr_local_config;
+ },
+ );
+
my $name = $test;
my $expected = read_file("./$test/expected_sdn_interfaces");
diff --git a/src/test/zones/evpn/frr_local_merge/expected_controller_config b/src/test/zones/evpn/frr_local_merge/expected_controller_config
new file mode 100644
index 000000000000..697daa20cdbd
--- /dev/null
+++ b/src/test/zones/evpn/frr_local_merge/expected_controller_config
@@ -0,0 +1,117 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor VTEP update-source dummy1
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ neighbor 192.168.1.1 remote-as 65001
+ neighbor 192.168.1.1 description "External Peer"
+ address-family ipv4 unicast
+ neighbor VTEP activate
+ exit-address-family
+ neighbor VTEP prefix-list MY_PREFIX_LIST out
+ neighbor VTEP allowas-in 1
+ neighbor VTEP remote-as 64600
+ no neighbor VTEP peer-group
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ advertise-svi-ip
+ no neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_IN_CUSTOM in
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+router isis isis1
+ net 47.0023.0000.0000.0000.0000.0000.0000.1900.0004.00
+ redistribute ipv4 connected level-1
+ redistribute ipv6 connected level-1
+ log-adjacency-changes
+exit
+!
+interface eth0
+ ip router isis isis1
+exit
+!
+interface eth1
+ ip router isis isis1
+exit
+route-map MAP_VTEP_IN permit 2
+ set community 65000:200
+exit
+!
+ip prefix-list PL_ALLOW seq 10 permit 10.0.0.0/8 le 24
+route-map CUSTOM_MAP permit 10
+ match ip address prefix-list PL_ALLOW
+exit
+!
+bgp community-list standard CL_LOCAL permit 65000:200
+interface iface2
+ ip ospf area 0
+exit
+!
+interface ens19
+ no ip ospf passive
+exit
+!
+router ospf
+ passive-interface default
+exit
+!
+!
+router ospf
+ ospf router-id 172.20.30.1
+exit
+!
+interface dummy_test
+ ip ospf area 0
+ ip ospf passive
+exit
+!
+interface ens19
+ ip ospf area 0
+exit
+!
+access-list pve_ospf_test_ips permit 172.20.30.0/24
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+ set community 65000:100
+exit
+!
+route-map pve_ospf permit 100
+ match ip address pve_ospf_test_ips
+ set src 172.20.30.1
+exit
+!
+ip protocol ospf route-map pve_ospf
+!
+line vty
+!
diff --git a/src/test/zones/evpn/frr_local_merge/expected_sdn_interfaces b/src/test/zones/evpn/frr_local_merge/expected_sdn_interfaces
new file mode 100644
index 000000000000..c7ddf44ef6d3
--- /dev/null
+++ b/src/test/zones/evpn/frr_local_merge/expected_sdn_interfaces
@@ -0,0 +1,53 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ hwaddress A2:1D:CB:1A:C0:8B
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto dummy_test
+iface dummy_test inet static
+ address 172.20.30.1/32
+ link-type dummy
+ ip-forward 1
+
+auto ens19
+iface ens19 inet static
+ address 172.16.3.10/31
+ ip-forward 1
diff --git a/src/test/zones/evpn/frr_local_merge/frr.conf.local b/src/test/zones/evpn/frr_local_merge/frr.conf.local
new file mode 100644
index 000000000000..c1ade80c1ea7
--- /dev/null
+++ b/src/test/zones/evpn/frr_local_merge/frr.conf.local
@@ -0,0 +1,59 @@
+!
+! Custom FRR configuration to be merged
+!
+ip nht resolve-via-default
+!
+ip route 192.0.2.0/24 198.51.100.1
+!
+ip protocol bgp route-map correct_src
+!
+router bgp 65000
+ neighbor 192.168.1.1 remote-as 65001
+ neighbor 192.168.1.1 description "External Peer"
+ address-family l2vpn evpn
+ advertise-svi-ip
+ exit-address-family
+ address-family ipv4 unicast
+ neighbor VTEP activate
+ exit-address-family
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+ set community 65000:100
+exit
+!
+route-map MAP_VTEP_IN permit 2
+ set community 65000:200
+exit
+!
+ip prefix-list PL_ALLOW seq 10 permit 10.0.0.0/8 le 24
+!
+route-map CUSTOM_MAP permit 10
+ match ip address prefix-list PL_ALLOW
+exit
+!
+bgp community-list standard CL_LOCAL permit 65000:200
+!
+interface iface2
+ ip ospf area 0
+exit
+!
+interface ens19
+ no ip ospf passive
+exit
+!
+router ospf
+ passive-interface default
+exit
+!
+router bgp 65000
+ neighbor VTEP prefix-list MY_PREFIX_LIST out
+ neighbor VTEP allowas-in 1
+ neighbor VTEP remote-as 64600
+ no neighbor VTEP peer-group
+ address-family l2vpn evpn
+ no neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_IN_CUSTOM in
+ exit-address-family
+exit
+!
diff --git a/src/test/zones/evpn/frr_local_merge/interfaces b/src/test/zones/evpn/frr_local_merge/interfaces
new file mode 100644
index 000000000000..66bb826a44b3
--- /dev/null
+++ b/src/test/zones/evpn/frr_local_merge/interfaces
@@ -0,0 +1,7 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/frr_local_merge/sdn_config b/src/test/zones/evpn/frr_local_merge/sdn_config
new file mode 100644
index 000000000000..17b022b10341
--- /dev/null
+++ b/src/test/zones/evpn/frr_local_merge/sdn_config
@@ -0,0 +1,73 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => { tag => "100", type => "vnet", zone => "myzone" },
+ },
+ },
+
+ zones => {
+ ids => { myzone => { ipam => "pve", type => "evpn", controller => "evpnctl", 'vrf-vxlan' => 1000, 'mac' => 'A2:1D:CB:1A:C0:8B' } },
+ },
+ controllers => {
+ ids => {
+ evpnctl => {
+ type => "evpn",
+ 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3',
+ asn => "65000"
+ },
+ localhost => {
+ type => "isis",
+ 'isis-domain' => 'isis1',
+ 'isis-ifaces' => 'eth1,eth0',
+ 'isis-net' => "47.0023.0000.0000.0000.0000.0000.0000.1900.0004.00",
+ loopback => 'dummy1',
+ node => "localhost",
+ },
+ },
+ },
+
+ subnets => {
+ ids => {
+ 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ }
+ }
+ },
+ fabrics => {
+ ids => {
+ test_pathfinder => {
+ id => 'test_pathfinder',
+ interfaces => [
+ 'name=ens19,ip=172.16.3.20/31'
+ ],
+ ip => '172.20.30.2',
+ type => 'ospf_node'
+ },
+ test => {
+ ip_prefix => '172.20.30.0/24',
+ area => '0',
+ type => 'ospf_fabric',
+ id => 'test',
+ },
+ test_localhost => {
+ id => 'test_localhost',
+ interfaces => [
+ 'name=ens19,ip=172.16.3.10/31'
+ ],
+ ip => '172.20.30.1',
+ type => 'ospf_node'
+ },
+ test_raider => {
+ type => 'ospf_node',
+ ip => '172.20.30.3',
+ id => 'test_raider',
+ interfaces => [
+ 'name=ens19,ip=172.16.3.30/31'
+ ]
+ }
+ }
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH pve-network v2 9/9] test: bgp: add some various integration tests
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (16 preceding siblings ...)
2026-03-02 12:55 ` [PATCH pve-network v2 8/9] test: add test for frr.conf.local merging Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
2026-03-02 12:55 ` [PATCH pve-manager v2 1/1] sdn: add dry-run diff view for sdn apply Gabriel Goller
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
Add a few tests that should cover most of the options possible in the
bgp and evpn plugin. Also add a tests in combination with isis.
The bgp_ebgp_reverse_order tests the case where the evpn and bgp controller
processing order is reversed (This leads to different configs).
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
.../evpn/bgp_ebgp/expected_controller_config | 54 +++++++++++++
.../evpn/bgp_ebgp/expected_sdn_interfaces | 41 ++++++++++
src/test/zones/evpn/bgp_ebgp/interfaces | 7 ++
src/test/zones/evpn/bgp_ebgp/sdn_config | 49 ++++++++++++
.../expected_controller_config | 67 ++++++++++++++++
.../bgp_ebgp_multihop/expected_sdn_interfaces | 41 ++++++++++
.../zones/evpn/bgp_ebgp_multihop/interfaces | 10 +++
.../zones/evpn/bgp_ebgp_multihop/sdn_config | 51 +++++++++++++
.../expected_controller_config | 52 +++++++++++++
.../expected_sdn_interfaces | 41 ++++++++++
.../evpn/bgp_ebgp_reverse_order/interfaces | 7 ++
.../evpn/bgp_ebgp_reverse_order/sdn_config | 49 ++++++++++++
.../bgp_loopback/expected_controller_config | 65 ++++++++++++++++
.../evpn/bgp_loopback/expected_sdn_interfaces | 41 ++++++++++
src/test/zones/evpn/bgp_loopback/interfaces | 10 +++
src/test/zones/evpn/bgp_loopback/sdn_config | 49 ++++++++++++
.../expected_controller_config | 55 ++++++++++++++
.../expected_sdn_interfaces | 41 ++++++++++
.../zones/evpn/bgp_multipath_relax/interfaces | 7 ++
.../zones/evpn/bgp_multipath_relax/sdn_config | 49 ++++++++++++
.../expected_controller_config | 76 +++++++++++++++++++
.../combined_bgp_isis/expected_sdn_interfaces | 41 ++++++++++
.../zones/evpn/combined_bgp_isis/interfaces | 10 +++
.../zones/evpn/combined_bgp_isis/sdn_config | 57 ++++++++++++++
.../evpn/ebgp_only/expected_controller_config | 25 ++++++
.../evpn/ebgp_only/expected_sdn_interfaces | 1 +
src/test/zones/evpn/ebgp_only/interfaces | 7 ++
src/test/zones/evpn/ebgp_only/sdn_config | 19 +++++
28 files changed, 1022 insertions(+)
create mode 100644 src/test/zones/evpn/bgp_ebgp/expected_controller_config
create mode 100644 src/test/zones/evpn/bgp_ebgp/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/bgp_ebgp/interfaces
create mode 100644 src/test/zones/evpn/bgp_ebgp/sdn_config
create mode 100644 src/test/zones/evpn/bgp_ebgp_multihop/expected_controller_config
create mode 100644 src/test/zones/evpn/bgp_ebgp_multihop/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/bgp_ebgp_multihop/interfaces
create mode 100644 src/test/zones/evpn/bgp_ebgp_multihop/sdn_config
create mode 100644 src/test/zones/evpn/bgp_ebgp_reverse_order/expected_controller_config
create mode 100644 src/test/zones/evpn/bgp_ebgp_reverse_order/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/bgp_ebgp_reverse_order/interfaces
create mode 100644 src/test/zones/evpn/bgp_ebgp_reverse_order/sdn_config
create mode 100644 src/test/zones/evpn/bgp_loopback/expected_controller_config
create mode 100644 src/test/zones/evpn/bgp_loopback/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/bgp_loopback/interfaces
create mode 100644 src/test/zones/evpn/bgp_loopback/sdn_config
create mode 100644 src/test/zones/evpn/bgp_multipath_relax/expected_controller_config
create mode 100644 src/test/zones/evpn/bgp_multipath_relax/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/bgp_multipath_relax/interfaces
create mode 100644 src/test/zones/evpn/bgp_multipath_relax/sdn_config
create mode 100644 src/test/zones/evpn/combined_bgp_isis/expected_controller_config
create mode 100644 src/test/zones/evpn/combined_bgp_isis/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/combined_bgp_isis/interfaces
create mode 100644 src/test/zones/evpn/combined_bgp_isis/sdn_config
create mode 100644 src/test/zones/evpn/ebgp_only/expected_controller_config
create mode 100644 src/test/zones/evpn/ebgp_only/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/ebgp_only/interfaces
create mode 100644 src/test/zones/evpn/ebgp_only/sdn_config
diff --git a/src/test/zones/evpn/bgp_ebgp/expected_controller_config b/src/test/zones/evpn/bgp_ebgp/expected_controller_config
new file mode 100644
index 000000000000..037079654b0f
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp/expected_controller_config
@@ -0,0 +1,54 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ neighbor BGP peer-group
+ neighbor BGP remote-as external
+ neighbor BGP bfd
+ neighbor 192.168.0.252 peer-group BGP
+ neighbor 192.168.0.253 peer-group BGP
+ !
+ address-family ipv4 unicast
+ neighbor BGP activate
+ neighbor BGP soft-reconfiguration inbound
+ exit-address-family
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/bgp_ebgp/expected_sdn_interfaces b/src/test/zones/evpn/bgp_ebgp/expected_sdn_interfaces
new file mode 100644
index 000000000000..4cf13e05e688
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp/expected_sdn_interfaces
@@ -0,0 +1,41 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
diff --git a/src/test/zones/evpn/bgp_ebgp/interfaces b/src/test/zones/evpn/bgp_ebgp/interfaces
new file mode 100644
index 000000000000..66bb826a44b3
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp/interfaces
@@ -0,0 +1,7 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/bgp_ebgp/sdn_config b/src/test/zones/evpn/bgp_ebgp/sdn_config
new file mode 100644
index 000000000000..0310e32db93c
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp/sdn_config
@@ -0,0 +1,49 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => {
+ tag => "100",
+ type => "vnet",
+ zone => "myzone",
+ },
+ },
+ },
+
+ zones => {
+ ids => {
+ myzone => {
+ ipam => "pve",
+ type => "evpn",
+ controller => "evpnctl",
+ 'vrf-vxlan' => 1000,
+ },
+ },
+ },
+ controllers => {
+ ids => {
+ evpnctl => {
+ type => "evpn",
+ 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3',
+ asn => "65000",
+ },
+ localhost => {
+ type => "bgp",
+ 'peers' => '192.168.0.252,192.168.0.253',
+ ebgp => "1",
+ asn => "65000",
+ node => "localhost",
+ },
+ },
+ },
+
+ subnets => {
+ ids => {
+ 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ },
+ },
+ },
+}
diff --git a/src/test/zones/evpn/bgp_ebgp_multihop/expected_controller_config b/src/test/zones/evpn/bgp_ebgp_multihop/expected_controller_config
new file mode 100644
index 000000000000..5da2ac744be1
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp_multihop/expected_controller_config
@@ -0,0 +1,67 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ bgp disable-ebgp-connected-route-check
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor VTEP update-source lo
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ neighbor BGP peer-group
+ neighbor BGP remote-as external
+ neighbor BGP bfd
+ neighbor BGP ebgp-multihop 5
+ neighbor 192.168.0.252 peer-group BGP
+ neighbor 192.168.0.253 peer-group BGP
+ !
+ address-family ipv4 unicast
+ network 192.168.0.1/32
+ neighbor BGP activate
+ neighbor BGP soft-reconfiguration inbound
+ exit-address-family
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+ip prefix-list loopbacks_ips seq 10 permit 0.0.0.0/0 le 32
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+!
+route-map correct_src permit 1
+ match ip address prefix-list loopbacks_ips
+ set src 192.168.0.1
+exit
+!
+ip protocol bgp route-map correct_src
+!
+line vty
+!
diff --git a/src/test/zones/evpn/bgp_ebgp_multihop/expected_sdn_interfaces b/src/test/zones/evpn/bgp_ebgp_multihop/expected_sdn_interfaces
new file mode 100644
index 000000000000..4cf13e05e688
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp_multihop/expected_sdn_interfaces
@@ -0,0 +1,41 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
diff --git a/src/test/zones/evpn/bgp_ebgp_multihop/interfaces b/src/test/zones/evpn/bgp_ebgp_multihop/interfaces
new file mode 100644
index 000000000000..af15fa4cb516
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp_multihop/interfaces
@@ -0,0 +1,10 @@
+auto lo
+iface lo inet loopback
+
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/bgp_ebgp_multihop/sdn_config b/src/test/zones/evpn/bgp_ebgp_multihop/sdn_config
new file mode 100644
index 000000000000..ff7e038eddcd
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp_multihop/sdn_config
@@ -0,0 +1,51 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => {
+ tag => "100",
+ type => "vnet",
+ zone => "myzone",
+ },
+ },
+ },
+
+ zones => {
+ ids => {
+ myzone => {
+ ipam => "pve",
+ type => "evpn",
+ controller => "evpnctl",
+ 'vrf-vxlan' => 1000,
+ },
+ },
+ },
+ controllers => {
+ ids => {
+ evpnctl => {
+ type => "evpn",
+ 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3',
+ asn => "65000",
+ },
+ localhost => {
+ type => "bgp",
+ 'peers' => '192.168.0.252,192.168.0.253',
+ ebgp => "1",
+ 'ebgp-multihop' => '5',
+ loopback => "lo",
+ asn => "65000",
+ node => "localhost",
+ },
+ },
+ },
+
+ subnets => {
+ ids => {
+ 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ },
+ },
+ },
+}
diff --git a/src/test/zones/evpn/bgp_ebgp_reverse_order/expected_controller_config b/src/test/zones/evpn/bgp_ebgp_reverse_order/expected_controller_config
new file mode 100644
index 000000000000..4857dc8a502f
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp_reverse_order/expected_controller_config
@@ -0,0 +1,52 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ neighbor BGP peer-group
+ neighbor BGP remote-as external
+ neighbor BGP bfd
+ neighbor 192.168.0.252 peer-group BGP
+ neighbor 192.168.0.253 peer-group BGP
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ !
+ address-family ipv4 unicast
+ neighbor BGP activate
+ neighbor BGP soft-reconfiguration inbound
+ exit-address-family
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/bgp_ebgp_reverse_order/expected_sdn_interfaces b/src/test/zones/evpn/bgp_ebgp_reverse_order/expected_sdn_interfaces
new file mode 100644
index 000000000000..4cf13e05e688
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp_reverse_order/expected_sdn_interfaces
@@ -0,0 +1,41 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
diff --git a/src/test/zones/evpn/bgp_ebgp_reverse_order/interfaces b/src/test/zones/evpn/bgp_ebgp_reverse_order/interfaces
new file mode 100644
index 000000000000..a2393416a399
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp_reverse_order/interfaces
@@ -0,0 +1,7 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/bgp_ebgp_reverse_order/sdn_config b/src/test/zones/evpn/bgp_ebgp_reverse_order/sdn_config
new file mode 100644
index 000000000000..e0e7ae788a90
--- /dev/null
+++ b/src/test/zones/evpn/bgp_ebgp_reverse_order/sdn_config
@@ -0,0 +1,49 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => {
+ tag => "100",
+ type => "vnet",
+ zone => "myzone",
+ },
+ },
+ },
+
+ zones => {
+ ids => {
+ myzone => {
+ ipam => "pve",
+ type => "evpn",
+ controller => "evpnctl",
+ 'vrf-vxlan' => 1000,
+ },
+ },
+ },
+ controllers => {
+ ids => {
+ blocalhost => {
+ type => "bgp",
+ 'peers' => '192.168.0.252,192.168.0.253',
+ ebgp => "1",
+ asn => "65000",
+ node => "localhost",
+ },
+ evpnctl => {
+ type => "evpn",
+ 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3',
+ asn => "65000",
+ },
+ },
+ },
+
+ subnets => {
+ ids => {
+ 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ },
+ },
+ },
+}
diff --git a/src/test/zones/evpn/bgp_loopback/expected_controller_config b/src/test/zones/evpn/bgp_loopback/expected_controller_config
new file mode 100644
index 000000000000..a2d2db04d28d
--- /dev/null
+++ b/src/test/zones/evpn/bgp_loopback/expected_controller_config
@@ -0,0 +1,65 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor VTEP update-source lo
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ neighbor BGP peer-group
+ neighbor BGP remote-as 65000
+ neighbor BGP bfd
+ neighbor 192.168.0.252 peer-group BGP
+ neighbor 192.168.0.253 peer-group BGP
+ !
+ address-family ipv4 unicast
+ network 192.168.0.1/32
+ neighbor BGP activate
+ neighbor BGP soft-reconfiguration inbound
+ exit-address-family
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+ip prefix-list loopbacks_ips seq 10 permit 0.0.0.0/0 le 32
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+!
+route-map correct_src permit 1
+ match ip address prefix-list loopbacks_ips
+ set src 192.168.0.1
+exit
+!
+ip protocol bgp route-map correct_src
+!
+line vty
+!
diff --git a/src/test/zones/evpn/bgp_loopback/expected_sdn_interfaces b/src/test/zones/evpn/bgp_loopback/expected_sdn_interfaces
new file mode 100644
index 000000000000..4cf13e05e688
--- /dev/null
+++ b/src/test/zones/evpn/bgp_loopback/expected_sdn_interfaces
@@ -0,0 +1,41 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
diff --git a/src/test/zones/evpn/bgp_loopback/interfaces b/src/test/zones/evpn/bgp_loopback/interfaces
new file mode 100644
index 000000000000..af15fa4cb516
--- /dev/null
+++ b/src/test/zones/evpn/bgp_loopback/interfaces
@@ -0,0 +1,10 @@
+auto lo
+iface lo inet loopback
+
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/bgp_loopback/sdn_config b/src/test/zones/evpn/bgp_loopback/sdn_config
new file mode 100644
index 000000000000..852dbdf6803a
--- /dev/null
+++ b/src/test/zones/evpn/bgp_loopback/sdn_config
@@ -0,0 +1,49 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => {
+ tag => "100",
+ type => "vnet",
+ zone => "myzone",
+ },
+ },
+ },
+
+ zones => {
+ ids => {
+ myzone => {
+ ipam => "pve",
+ type => "evpn",
+ controller => "evpnctl",
+ 'vrf-vxlan' => 1000,
+ },
+ },
+ },
+ controllers => {
+ ids => {
+ evpnctl => {
+ type => "evpn",
+ 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3',
+ asn => "65000",
+ },
+ localhost => {
+ type => "bgp",
+ 'peers' => '192.168.0.252,192.168.0.253',
+ loopback => "lo",
+ asn => "65000",
+ node => "localhost",
+ },
+ },
+ },
+
+ subnets => {
+ ids => {
+ 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ },
+ },
+ },
+}
diff --git a/src/test/zones/evpn/bgp_multipath_relax/expected_controller_config b/src/test/zones/evpn/bgp_multipath_relax/expected_controller_config
new file mode 100644
index 000000000000..a8a5a85e060e
--- /dev/null
+++ b/src/test/zones/evpn/bgp_multipath_relax/expected_controller_config
@@ -0,0 +1,55 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ bgp bestpath as-path multipath-relax
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ neighbor BGP peer-group
+ neighbor BGP remote-as 65000
+ neighbor BGP bfd
+ neighbor 192.168.0.252 peer-group BGP
+ neighbor 192.168.0.253 peer-group BGP
+ !
+ address-family ipv4 unicast
+ neighbor BGP activate
+ neighbor BGP soft-reconfiguration inbound
+ exit-address-family
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/bgp_multipath_relax/expected_sdn_interfaces b/src/test/zones/evpn/bgp_multipath_relax/expected_sdn_interfaces
new file mode 100644
index 000000000000..4cf13e05e688
--- /dev/null
+++ b/src/test/zones/evpn/bgp_multipath_relax/expected_sdn_interfaces
@@ -0,0 +1,41 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
diff --git a/src/test/zones/evpn/bgp_multipath_relax/interfaces b/src/test/zones/evpn/bgp_multipath_relax/interfaces
new file mode 100644
index 000000000000..66bb826a44b3
--- /dev/null
+++ b/src/test/zones/evpn/bgp_multipath_relax/interfaces
@@ -0,0 +1,7 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/bgp_multipath_relax/sdn_config b/src/test/zones/evpn/bgp_multipath_relax/sdn_config
new file mode 100644
index 000000000000..9513906a47e0
--- /dev/null
+++ b/src/test/zones/evpn/bgp_multipath_relax/sdn_config
@@ -0,0 +1,49 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => {
+ tag => "100",
+ type => "vnet",
+ zone => "myzone",
+ },
+ },
+ },
+
+ zones => {
+ ids => {
+ myzone => {
+ ipam => "pve",
+ type => "evpn",
+ controller => "evpnctl",
+ 'vrf-vxlan' => 1000,
+ },
+ },
+ },
+ controllers => {
+ ids => {
+ evpnctl => {
+ type => "evpn",
+ 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3',
+ asn => "65000",
+ },
+ localhost => {
+ type => "bgp",
+ 'peers' => '192.168.0.252,192.168.0.253',
+ 'bgp-multipath-as-path-relax' => "1",
+ asn => "65000",
+ node => "localhost",
+ },
+ },
+ },
+
+ subnets => {
+ ids => {
+ 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ },
+ },
+ },
+}
diff --git a/src/test/zones/evpn/combined_bgp_isis/expected_controller_config b/src/test/zones/evpn/combined_bgp_isis/expected_controller_config
new file mode 100644
index 000000000000..427dc797c3b2
--- /dev/null
+++ b/src/test/zones/evpn/combined_bgp_isis/expected_controller_config
@@ -0,0 +1,76 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor VTEP update-source lo
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ neighbor BGP peer-group
+ neighbor BGP remote-as 65000
+ neighbor BGP bfd
+ neighbor 192.168.0.252 peer-group BGP
+ neighbor 192.168.0.253 peer-group BGP
+ !
+ address-family ipv4 unicast
+ network 192.168.0.1/32
+ neighbor BGP activate
+ neighbor BGP soft-reconfiguration inbound
+ exit-address-family
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+router isis isis1
+ net 47.0023.0000.0000.0000.0000.0000.0000.1900.0004.00
+ redistribute ipv4 connected level-1
+ redistribute ipv6 connected level-1
+ log-adjacency-changes
+exit
+!
+interface eth1
+ ip router isis isis1
+exit
+!
+ip prefix-list loopbacks_ips seq 10 permit 0.0.0.0/0 le 32
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+!
+route-map correct_src permit 1
+ match ip address prefix-list loopbacks_ips
+ set src 192.168.0.1
+exit
+!
+ip protocol bgp route-map correct_src
+!
+line vty
+!
diff --git a/src/test/zones/evpn/combined_bgp_isis/expected_sdn_interfaces b/src/test/zones/evpn/combined_bgp_isis/expected_sdn_interfaces
new file mode 100644
index 000000000000..4cf13e05e688
--- /dev/null
+++ b/src/test/zones/evpn/combined_bgp_isis/expected_sdn_interfaces
@@ -0,0 +1,41 @@
+#version:1
+
+auto myvnet
+iface myvnet
+ address 10.0.0.1/24
+ bridge_ports vxlan_myvnet
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+ vrf-table auto
+ post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+ bridge-ports vrfvx_myzone
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+ vxlan-id 1000
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+ vxlan-id 100
+ vxlan-local-tunnelip 192.168.0.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
diff --git a/src/test/zones/evpn/combined_bgp_isis/interfaces b/src/test/zones/evpn/combined_bgp_isis/interfaces
new file mode 100644
index 000000000000..af15fa4cb516
--- /dev/null
+++ b/src/test/zones/evpn/combined_bgp_isis/interfaces
@@ -0,0 +1,10 @@
+auto lo
+iface lo inet loopback
+
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/combined_bgp_isis/sdn_config b/src/test/zones/evpn/combined_bgp_isis/sdn_config
new file mode 100644
index 000000000000..d4c03136dab8
--- /dev/null
+++ b/src/test/zones/evpn/combined_bgp_isis/sdn_config
@@ -0,0 +1,57 @@
+{
+ version => 1,
+ vnets => {
+ ids => {
+ myvnet => {
+ tag => "100",
+ type => "vnet",
+ zone => "myzone",
+ },
+ },
+ },
+
+ zones => {
+ ids => {
+ myzone => {
+ ipam => "pve",
+ type => "evpn",
+ controller => "evpnctl",
+ 'vrf-vxlan' => 1000,
+ },
+ },
+ },
+ controllers => {
+ ids => {
+ evpnctl => {
+ type => "evpn",
+ 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3',
+ asn => "65000",
+ },
+ localhost => {
+ type => "bgp",
+ 'peers' => '192.168.0.252,192.168.0.253',
+ loopback => "lo",
+ asn => "65000",
+ node => "localhost",
+ },
+ isisctl => {
+ type => "isis",
+ 'isis-domain' => 'isis1',
+ 'isis-ifaces' => 'eth1',
+ 'isis-net' => "47.0023.0000.0000.0000.0000.0000.0000.1900.0004.00",
+ loopback => "lo",
+ node => "localhost",
+ },
+ },
+ },
+
+ subnets => {
+ ids => {
+ 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ },
+ },
+ },
+}
diff --git a/src/test/zones/evpn/ebgp_only/expected_controller_config b/src/test/zones/evpn/ebgp_only/expected_controller_config
new file mode 100644
index 000000000000..83910dfc2157
--- /dev/null
+++ b/src/test/zones/evpn/ebgp_only/expected_controller_config
@@ -0,0 +1,25 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+router bgp 65001
+ bgp router-id 192.168.0.1
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ neighbor BGP peer-group
+ neighbor BGP remote-as external
+ neighbor BGP bfd
+ neighbor BGP ebgp-multihop 3
+ neighbor 192.168.0.252 peer-group BGP
+ neighbor 192.168.0.253 peer-group BGP
+ !
+ address-family ipv4 unicast
+ neighbor BGP activate
+ neighbor BGP soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/ebgp_only/expected_sdn_interfaces b/src/test/zones/evpn/ebgp_only/expected_sdn_interfaces
new file mode 100644
index 000000000000..edc8ff918531
--- /dev/null
+++ b/src/test/zones/evpn/ebgp_only/expected_sdn_interfaces
@@ -0,0 +1 @@
+#version:1
diff --git a/src/test/zones/evpn/ebgp_only/interfaces b/src/test/zones/evpn/ebgp_only/interfaces
new file mode 100644
index 000000000000..66bb826a44b3
--- /dev/null
+++ b/src/test/zones/evpn/ebgp_only/interfaces
@@ -0,0 +1,7 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/ebgp_only/sdn_config b/src/test/zones/evpn/ebgp_only/sdn_config
new file mode 100644
index 000000000000..73d323f7b127
--- /dev/null
+++ b/src/test/zones/evpn/ebgp_only/sdn_config
@@ -0,0 +1,19 @@
+{
+ version => 1,
+ vnets => {},
+ zones => {},
+ controllers => {
+ ids => {
+ localhost => {
+ type => "bgp",
+ 'peers' => '192.168.0.252,192.168.0.253',
+ ebgp => "1",
+ 'ebgp-multihop' => '3',
+ asn => "65001",
+ node => "localhost",
+ },
+ },
+ },
+
+ subnets => {},
+}
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread* [PATCH pve-manager v2 1/1] sdn: add dry-run diff view for sdn apply
2026-03-02 12:55 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v2 00/19] Generate frr config using jinja templates and rust types Gabriel Goller
` (17 preceding siblings ...)
2026-03-02 12:55 ` [PATCH pve-network v2 9/9] test: bgp: add some various integration tests Gabriel Goller
@ 2026-03-02 12:55 ` Gabriel Goller
18 siblings, 0 replies; 22+ messages in thread
From: Gabriel Goller @ 2026-03-02 12:55 UTC (permalink / raw)
To: pve-devel
Introduce a new SdnDiffView modal that runs a dry-run and shows the frr
and ifupdown2 configuration changes which will be made when clicking
apply. Now the user knows which config options will be set without
needing to apply the config.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/sdn/SdnDiffView.js | 139 ++++++++++++++++++++++++++++++++
www/manager6/sdn/StatusView.js | 8 ++
3 files changed, 148 insertions(+)
create mode 100644 www/manager6/sdn/SdnDiffView.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 4558d53e54be..da602523b27a 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -286,6 +286,7 @@ JSSRC= \
sdn/ControllerView.js \
sdn/Status.js \
sdn/StatusView.js \
+ sdn/SdnDiffView.js \
sdn/VnetEdit.js \
sdn/VnetView.js \
sdn/VnetACLView.js \
diff --git a/www/manager6/sdn/SdnDiffView.js b/www/manager6/sdn/SdnDiffView.js
new file mode 100644
index 000000000000..bfff3675950d
--- /dev/null
+++ b/www/manager6/sdn/SdnDiffView.js
@@ -0,0 +1,139 @@
+Ext.define('PVE.sdn.SdnDiffView', {
+ extend: 'Ext.window.Window',
+
+ width: 800,
+ height: 900,
+
+ scrollable: true,
+ modal: true,
+ title: gettext('Pending SDN configuration changes'),
+
+ node: undefined,
+
+ viewModel: {
+ data: {
+ frr_diff: undefined,
+ interfaces_diff: undefined,
+ },
+ },
+
+ items: [
+ {
+ xtype: 'displayfield',
+ padding: 10,
+ fieldLabel: gettext('FRR config'),
+ bind: {
+ value: '{frr_diff}',
+ },
+ },
+ {
+ xtype: 'displayfield',
+ padding: 10,
+ fieldLabel: gettext('Interfaces config'),
+ bind: {
+ value: '{interfaces_diff}',
+ },
+ },
+ ],
+ buttons: [
+ {
+ handler: function () {
+ this.up('window').close();
+ },
+ text: gettext('Close'),
+ },
+ ],
+
+ loadDiff: async function () {
+ let me = this;
+
+ let req = await Proxmox.Async.api2({
+ url: `/cluster/sdn/dry-run`,
+ params: { node: me.node },
+ method: 'PUT',
+ });
+
+ return req.result.data;
+ },
+
+ load: function () {
+ let me = this;
+
+ me.setLoading('fetching node diff');
+
+ me.loadDiff()
+ .catch(Proxmox.Utils.alertResponseFailure)
+ .then((diff) => {
+ if (diff['frr-diff'] === null) {
+ this.getViewModel().set('frr_diff', '');
+ } else {
+ this.getViewModel().set('frr_diff', '<pre>' + diff['frr-diff'] + '</pre>');
+ }
+ if (diff['interfaces-diff'] === null) {
+ this.getViewModel().set('interfaces_diff', '');
+ } else {
+ this.getViewModel().set(
+ 'interfaces_diff',
+ '<pre>' + diff['interfaces-diff'] + '</pre>',
+ );
+ }
+ })
+ .finally(() => {
+ me.setLoading(false);
+ });
+ },
+
+ getNodeSelector: function () {
+ let me = this;
+
+ return Ext.create('PVE.form.NodeSelector', {
+ xtype: 'pveNodeSelector',
+ reference: 'nodeselector',
+ fieldLabel: gettext('Node'),
+ padding: 10,
+ labelWidth: 120,
+ name: 'node',
+ allowBlank: false,
+ listeners: {
+ change: function (f, value) {
+ me.node = value;
+ me.load();
+ },
+ },
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Node'),
+ dataIndex: 'node',
+ sortable: true,
+ hideable: false,
+ flex: 1,
+ },
+ ],
+ },
+ store: {
+ fields: ['node'],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes',
+ },
+ sorters: [
+ {
+ property: 'node',
+ direction: 'ASC',
+ },
+ ],
+ },
+ });
+ },
+
+ initComponent: function () {
+ let me = this;
+
+ me.nodeSelector = me.getNodeSelector();
+
+ me.items = [me.nodeSelector, ...me.items];
+
+ me.callParent();
+ },
+});
diff --git a/www/manager6/sdn/StatusView.js b/www/manager6/sdn/StatusView.js
index fbc712c6cf6b..fada50411e91 100644
--- a/www/manager6/sdn/StatusView.js
+++ b/www/manager6/sdn/StatusView.js
@@ -69,6 +69,14 @@ Ext.define(
});
},
},
+ {
+ text: gettext('Dry-Run'),
+ handler: function () {
+ Ext.create('PVE.sdn.SdnDiffView', {
+ autoShow: true,
+ });
+ },
+ },
],
viewConfig: {
trackOver: false,
--
2.47.3
^ permalink raw reply [flat|nested] 22+ messages in thread