* [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN
@ 2026-03-25 9:41 Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-cluster 1/2] cfs: add 'sdn/route-maps.cfg' to observed files Stefan Hanreich
` (28 more replies)
0 siblings, 29 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
## Introduction
This patch adds support for managing route maps and prefix lists to the SDN
stack. With this patch series, route maps can be applied to the BGP and EVPN
controller for incoming / outgoing route filtering. There are currently some
other features in development that would make use of route maps as well, namely:
* VRF route leaking
* Route Redistribution for Fabrics
Prefix Lists can be used for matching inside route map match statements. They
are implemented so they can be used inside route map match statements for now.
## Motivation
There are a lot of use-cases for enabling users to create their own route-maps,
which was currently only possible by utilizing frr.conf.local - which was clunky
and prone to issues. Route maps can be used for filtering in/outgoing routes and
modifiy them, so users could e.g. only selectively advertise some routes via BGP
or only import certain EVPN routes from outside.
It also allows us to programmatically manage route maps via the UI, e.g. for
deeper EVPN integration in PDM. This opens up a lot of possibilities for
new features.
## Configuration Format
This patch series adds two new configuration files, route-maps.cfg and
prefix-lists.cfg in /etc/pve/sdn.
### route-maps.cfg
An example route map configuration looks as follows:
route-map-entry: example_123
action permit
match key=vni,value=23487
set key=tag,value=23487
This would create the following FRR route map entry:
route-map example permit 123
match evpn vni 23487
set tag 23487
Every entry in route-maps.cfg maps to an entry in a route map. They are
identified by their name as well as their ordering number. `example_123`
specifies the 123th entry in the route map 'example'. The main reason for
choosing this format is, that having a single section for one route-map would be
quite unwieldy. It'd require some format like this, which is pretty awkward to
handle / validate:
route-map-entry: example
action permit,seq=123
match key=vni,value=23487,seq=123
set key=tag,value=23487,seq=123
>From a UI POV editing singular route map entries seems better as well, and with
the mapping of section entries to route map entries, a suitable API design
follows quite naturally and easily maps to the respective section config
entries, without too much data mangling required.
### prefix-lists.cfg
An example prefix list configuration looks as follows:
prefix-list: example-1
entries action=permit,prefix=192.0.2.0/24
entries action=permit,prefix=192.0.2.0/24,le=32
entries action=permit,prefix=192.0.2.0/24,le=32,ge=24,seq=123
This would create the following FRR prefix list:
ip prefix-list example-1 permit 192.0.2.0/24
ip prefix-list example-1 permit 192.0.2.0/24 le 32
ip prefix-list example-1 seq 123 permit 192.0.2.0/24 le 32 ge 24
## API endpoints
This patch series introduces the following API endpoints in the /cluster/sdn
subfolder:
### Route Maps
GET /route-maps - lists all route map entries
GET /route-maps/<id> - lists all route map entries for the route map <id>
GET /route-maps/<id>/<order> - gets the order'th entry in route map <id>
POST /route-maps - creates a new route map entry
PUT /route-maps/<id>/<order> - updates the order'th entry in route map <id>
DELETE /route-maps/<id>/<order> - deletes the order'th entry in route map <id>
### Prefix Lists
GET /prefix-lists - lists all prefix lists
GET /prefix-lists/<id> - get prefix list <id>
POST /prefix-lists - create a new prefix list
PUT /prefix-lists/<id> - update prefix list <id>
DELETE /prefix-lists/<id> - delete prefix list <id>
## Open questions
How should we handle overriding the auto-generated route maps (e.g. in the EVPN
controller) and prefix lists?
Currently this patch series disallows creating any route map / prefix list that
have the same name as PVE auto-generated ones via the API. They can be
overridden by creating a new route map and then selecting it in the respective
entity (e.g. via route-map-in in the EVPN controller). Pre-defined prefix-lists
cannot currently be overridden, since this usually makes little sense, as they
are used in the auto-generated route maps, which can be overriden anyway.
pve-cluster:
Stefan Hanreich (2):
cfs: add 'sdn/route-maps.cfg' to observed files
cfs: add 'sdn/prefix-lists.cfg' to observed files
src/PVE/Cluster.pm | 2 ++
src/pmxcfs/status.c | 2 ++
2 files changed, 4 insertions(+)
proxmox-ve-rs:
Stefan Hanreich (9):
sdn-types: add common route-map helper types
frr: implement routemap match/set statements via adjacent tagging
frr: allow rendering prefix-lists/route-maps separately
frr-templates: change route maps template to adapt to new types
ve-config: add prefix list section config
ve-config: frr: implement frr config generation for prefix lists
ve-config: add route map section config
ve-config: frr: implement frr config generation for route maps
ve-config: fabrics: adapt frr config generation to new format
.../templates/route_maps.jinja | 12 +-
proxmox-frr/Cargo.toml | 2 +-
proxmox-frr/src/ser/route_map.rs | 101 ++-
proxmox-frr/src/ser/serializer.rs | 35 +-
proxmox-sdn-types/src/bgp.rs | 50 ++
proxmox-sdn-types/src/lib.rs | 135 ++++
proxmox-ve-config/debian/control | 2 +
proxmox-ve-config/src/sdn/fabric/frr.rs | 25 +-
proxmox-ve-config/src/sdn/mod.rs | 2 +
proxmox-ve-config/src/sdn/prefix_list.rs | 347 ++++++++
proxmox-ve-config/src/sdn/route_map.rs | 762 ++++++++++++++++++
11 files changed, 1408 insertions(+), 65 deletions(-)
create mode 100644 proxmox-sdn-types/src/bgp.rs
create mode 100644 proxmox-ve-config/src/sdn/prefix_list.rs
create mode 100644 proxmox-ve-config/src/sdn/route_map.rs
proxmox-perl-rs:
Stefan Hanreich (3):
pve-rs: sdn: add route maps module
pve-rs: sdn: add prefix lists module
sdn: add prefix list / route maps to frr config generation helper
pve-rs/Cargo.toml | 1 +
pve-rs/Makefile | 2 +
pve-rs/src/bindings/sdn/mod.rs | 30 ++-
pve-rs/src/bindings/sdn/prefix_lists.rs | 199 +++++++++++++++++++
pve-rs/src/bindings/sdn/route_maps.rs | 243 ++++++++++++++++++++++++
5 files changed, 472 insertions(+), 3 deletions(-)
create mode 100644 pve-rs/src/bindings/sdn/prefix_lists.rs
create mode 100644 pve-rs/src/bindings/sdn/route_maps.rs
pve-network:
Stefan Hanreich (13):
controller: bgp: evpn: adapt to new match / set frr config syntax
sdn: add prefix lists module
api2: add prefix list module
sdn: add route map module
api2: add route maps api module
api2: add route map module
api2: add route map entry module
evpn controller: add route_map_{in,out} parameter
sdn: generate route map / prefix list configuration on sdn apply
tests: add simple route map test case
tests: add bgp evpn route map/prefix list testcase
tests: add route map with prefix list testcase
bgp controller: allow configuring custom route maps
src/PVE/API2/Network/SDN.pm | 14 +
src/PVE/API2/Network/SDN/Makefile | 13 +-
src/PVE/API2/Network/SDN/PrefixLists.pm | 247 ++++++++++++++++++
src/PVE/API2/Network/SDN/RouteMaps.pm | 137 ++++++++++
src/PVE/API2/Network/SDN/RouteMaps/Makefile | 9 +
.../API2/Network/SDN/RouteMaps/RouteMap.pm | 92 +++++++
.../Network/SDN/RouteMaps/RouteMapEntry.pm | 136 ++++++++++
src/PVE/Network/SDN.pm | 14 +-
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 22 +-
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 43 +--
src/PVE/Network/SDN/Controllers/Plugin.pm | 14 +
src/PVE/Network/SDN/Makefile | 14 +-
src/PVE/Network/SDN/PrefixLists.pm | 134 ++++++++++
src/PVE/Network/SDN/RouteMaps.pm | 173 ++++++++++++
.../expected_controller_config | 76 ++++++
.../expected_sdn_interfaces | 41 +++
.../bgp_evpn_routemap_prefix_list/interfaces | 7 +
.../bgp_evpn_routemap_prefix_list/sdn_config | 86 ++++++
.../evpn/routemap/expected_controller_config | 60 +++++
.../evpn/routemap/expected_sdn_interfaces | 41 +++
src/test/zones/evpn/routemap/interfaces | 7 +
src/test/zones/evpn/routemap/sdn_config | 70 +++++
.../expected_controller_config | 49 ++++
.../expected_sdn_interfaces | 41 +++
.../evpn/routemap_prefix_list/interfaces | 7 +
.../evpn/routemap_prefix_list/sdn_config | 58 ++++
26 files changed, 1572 insertions(+), 33 deletions(-)
create mode 100644 src/PVE/API2/Network/SDN/PrefixLists.pm
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps.pm
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/Makefile
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
create mode 100644 src/PVE/Network/SDN/PrefixLists.pm
create mode 100644 src/PVE/Network/SDN/RouteMaps.pm
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_controller_config
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/interfaces
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/sdn_config
create mode 100644 src/test/zones/evpn/routemap/expected_controller_config
create mode 100644 src/test/zones/evpn/routemap/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/routemap/interfaces
create mode 100644 src/test/zones/evpn/routemap/sdn_config
create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_controller_config
create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/routemap_prefix_list/interfaces
create mode 100644 src/test/zones/evpn/routemap_prefix_list/sdn_config
Summary over all repositories:
44 files changed, 3456 insertions(+), 101 deletions(-)
--
Generated by git-murpp 0.8.0
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-cluster 1/2] cfs: add 'sdn/route-maps.cfg' to observed files
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-cluster 2/2] cfs: add 'sdn/prefix-lists.cfg' " Stefan Hanreich
` (27 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Used for storing route maps to be used across different SDN entities,
for now BGP / EVPN controllers.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Cluster.pm | 1 +
src/pmxcfs/status.c | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
index bdb465f..88b40bf 100644
--- a/src/PVE/Cluster.pm
+++ b/src/PVE/Cluster.pm
@@ -83,6 +83,7 @@ my $observed = {
'sdn/mac-cache.json' => 1,
'sdn/dns.cfg' => 1,
'sdn/fabrics.cfg' => 1,
+ 'sdn/route-maps.cfg' => 1,
'sdn/.running-config' => 1,
'virtual-guest/cpu-models.conf' => 1,
'virtual-guest/profiles.cfg' => 1,
diff --git a/src/pmxcfs/status.c b/src/pmxcfs/status.c
index cb03e4e..aa9ce4b 100644
--- a/src/pmxcfs/status.c
+++ b/src/pmxcfs/status.c
@@ -110,6 +110,7 @@ static memdb_change_t memdb_change_array[] = {
{.path = "sdn/pve-ipam-state.json"},
{.path = "sdn/dns.cfg"},
{.path = "sdn/fabrics.cfg"},
+ {.path = "sdn/route-maps.cfg"},
{.path = "sdn/.running-config"},
{.path = "virtual-guest/cpu-models.conf"},
{.path = "virtual-guest/profiles.cfg"},
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-cluster 2/2] cfs: add 'sdn/prefix-lists.cfg' to observed files
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-cluster 1/2] cfs: add 'sdn/route-maps.cfg' to observed files Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 1/9] sdn-types: add common route-map helper types Stefan Hanreich
` (26 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Used for storing prefix lists to be used inside route maps.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Cluster.pm | 1 +
src/pmxcfs/status.c | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
index 88b40bf..9b2da79 100644
--- a/src/PVE/Cluster.pm
+++ b/src/PVE/Cluster.pm
@@ -84,6 +84,7 @@ my $observed = {
'sdn/dns.cfg' => 1,
'sdn/fabrics.cfg' => 1,
'sdn/route-maps.cfg' => 1,
+ 'sdn/prefix-lists.cfg' => 1,
'sdn/.running-config' => 1,
'virtual-guest/cpu-models.conf' => 1,
'virtual-guest/profiles.cfg' => 1,
diff --git a/src/pmxcfs/status.c b/src/pmxcfs/status.c
index aa9ce4b..19d45e0 100644
--- a/src/pmxcfs/status.c
+++ b/src/pmxcfs/status.c
@@ -111,6 +111,7 @@ static memdb_change_t memdb_change_array[] = {
{.path = "sdn/dns.cfg"},
{.path = "sdn/fabrics.cfg"},
{.path = "sdn/route-maps.cfg"},
+ {.path = "sdn/prefix-lists.cfg"},
{.path = "sdn/.running-config"},
{.path = "virtual-guest/cpu-models.conf"},
{.path = "virtual-guest/profiles.cfg"},
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-ve-rs 1/9] sdn-types: add common route-map helper types
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-cluster 1/2] cfs: add 'sdn/route-maps.cfg' to observed files Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-cluster 2/2] cfs: add 'sdn/prefix-lists.cfg' " Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 2/9] frr: implement routemap match/set statements via adjacent tagging Stefan Hanreich
` (25 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
The reason for including those types here is that they are used in
proxmox-frr for generating the FRR configuratiobn as well as
proxmox-ve-config for saving them inside a section config.
For some values in route maps FRR supports specifying either an
absolute value or a value relative to the existing value when
modifying a route via a route map. E.g. 123 would set the value to
123, whereas +/-123 would add/subtract 123 from the existing value.
IntegerWithSign can be used to represent such a value in the section
config.
Metric supports this notation as well as some magical values (e.g.
subtracting the round-trip-time).
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-sdn-types/src/bgp.rs | 50 +++++++++++++
proxmox-sdn-types/src/lib.rs | 135 +++++++++++++++++++++++++++++++++++
2 files changed, 185 insertions(+)
create mode 100644 proxmox-sdn-types/src/bgp.rs
diff --git a/proxmox-sdn-types/src/bgp.rs b/proxmox-sdn-types/src/bgp.rs
new file mode 100644
index 0000000..168bc1a
--- /dev/null
+++ b/proxmox-sdn-types/src/bgp.rs
@@ -0,0 +1,50 @@
+use serde::{Deserialize, Serialize};
+
+use crate::IntegerWithSign;
+
+/// Represents a BGP metric value, as used in FRR.
+///
+/// A metric can either be a numeric value, or certain 'magic' values. For more information see the
+/// respective enum variants.
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
+pub enum SetMetricValue {
+ /// Set the metric to the round-trip-time.
+ #[serde(rename = "rtt")]
+ Rtt,
+ /// Add the round-trip-time to the metric.
+ #[serde(rename = "+rtt")]
+ AddRtt,
+ /// Subtract the round-trip-time from the metric.
+ #[serde(rename = "-rtt")]
+ SubtractRtt,
+ /// Use the IGP value when importing from another IGP.
+ #[serde(rename = "igp")]
+ Igp,
+ /// Use the accumulated IGP value when importing from another IGP.
+ #[serde(rename = "aigp")]
+ Aigp,
+ /// Set the metric to a fixed numeric value.
+ #[serde(untagged)]
+ Numeric(IntegerWithSign),
+}
+
+/// An EVPN route-type, as used in the FRR route maps.
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "lowercase")]
+pub enum EvpnRouteType {
+ Ead,
+ MacIp,
+ Multicast,
+ #[serde(rename = "es")]
+ EthernetSegment,
+ Prefix,
+}
+
+/// An tag value, as used in the FRR route maps.
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(rename_all = "lowercase")]
+pub enum SetTagValue {
+ Untagged,
+ #[serde(untagged)]
+ Numeric(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+}
diff --git a/proxmox-sdn-types/src/lib.rs b/proxmox-sdn-types/src/lib.rs
index 1656f1d..fe4c641 100644
--- a/proxmox-sdn-types/src/lib.rs
+++ b/proxmox-sdn-types/src/lib.rs
@@ -1,3 +1,138 @@
pub mod area;
+pub mod bgp;
pub mod net;
pub mod openfabric;
+
+use serde::de::{Error, Visitor};
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::api;
+
+/// Enum for representing signedness of Integer in [`IntegerWithSign`].
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
+pub enum Sign {
+ #[serde(rename = "-")]
+ Negative,
+ #[serde(rename = "+")]
+ Positive,
+}
+
+proxmox_serde::forward_display_to_serialize!(Sign);
+proxmox_serde::forward_from_str_to_deserialize!(Sign);
+
+/// An Integer with an optional [`Sign`].
+///
+/// This is used for representing certain keys in the FRR route maps (e.g. metric). They can be set
+/// to either a static value (no sign) or to a value relative to the existing value (with sign).
+/// For instance, a value of 50 would set the metric to 50, but a value of +50 would add 50 to the
+/// existing metric value.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct IntegerWithSign {
+ pub(crate) sign: Option<Sign>,
+ pub(crate) n: u32,
+}
+
+impl std::fmt::Display for IntegerWithSign {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ if let Some(sign) = self.sign.as_ref() {
+ write!(f, "{}{}", sign, self.n)
+ } else {
+ self.n.fmt(f)
+ }
+ }
+}
+
+impl std::str::FromStr for IntegerWithSign {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
+ if let Some(n) = s.strip_prefix("+") {
+ return Ok(Self {
+ sign: Some(Sign::Positive),
+ n: n.parse()?,
+ });
+ }
+
+ if let Some(n) = s.strip_prefix("-") {
+ return Ok(Self {
+ sign: Some(Sign::Negative),
+ n: n.parse()?,
+ });
+ }
+
+ Ok(Self {
+ sign: None,
+ n: s.parse()?,
+ })
+ }
+}
+
+impl<'de> Deserialize<'de> for IntegerWithSign {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct V;
+
+ impl<'de> Visitor<'de> for V {
+ type Value = IntegerWithSign;
+
+ fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ f.write_str("An integer with an optional leading sign")
+ }
+
+ fn visit_i128<E: serde::de::Error>(self, value: i128) -> Result<Self::Value, E> {
+ Ok(IntegerWithSign {
+ sign: None,
+ n: u32::try_from(value).map_err(E::custom)?,
+ })
+ }
+
+ fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
+ Ok(IntegerWithSign {
+ sign: None,
+ n: u32::try_from(value).map_err(E::custom)?,
+ })
+ }
+
+ fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
+ Ok(IntegerWithSign {
+ sign: None,
+ n: u32::try_from(value).map_err(E::custom)?,
+ })
+ }
+
+ fn visit_u128<E: serde::de::Error>(self, value: u128) -> Result<Self::Value, E> {
+ Ok(IntegerWithSign {
+ sign: None,
+ n: u32::try_from(value).map_err(E::custom)?,
+ })
+ }
+
+ fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
+ v.parse().map_err(E::custom)
+ }
+ }
+
+ deserializer.deserialize_any(V)
+ }
+}
+
+proxmox_serde::forward_serialize_to_display!(IntegerWithSign);
+
+#[api(
+ type: Integer,
+ minimum: 1,
+ maximum: 16_777_215,
+)]
+#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
+#[repr(transparent)]
+/// Represents a VXLAN VNI (24-bit unsigned integer).
+pub struct Vni(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32);
+
+impl Vni {
+ /// Returns the VNI as u32.
+ pub fn as_u32(&self) -> u32 {
+ self.0
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-ve-rs 2/9] frr: implement routemap match/set statements via adjacent tagging
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (2 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 1/9] sdn-types: add common route-map helper types Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-26 14:44 ` Hannes Laimer
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 3/9] frr: allow rendering prefix-lists/route-maps separately Stefan Hanreich
` (24 subsequent siblings)
28 siblings, 1 reply; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Previously the types used a mix of adjacent / internal tagging and a
nesting of types to represent match and set statements. This has been
simplified by utilizing adjacent tagging on the set / match statements
and using the exact FRR configuration key as the tag. This way a
single enum can be used to represent match / set statements and all
variants can be rendered the same by simply printing the keys /
values.
This commit also adds a lot of new match / set statements that were
previously not supported. The crate supports now almost all match /
set statements that FRR supports - with only a few having been
omitted. Most notably it is not possible to match on community lists,
support for those is planned in a future patch series.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-frr/Cargo.toml | 2 +-
proxmox-frr/src/ser/route_map.rs | 101 +++++++++++++++++++------------
2 files changed, 63 insertions(+), 40 deletions(-)
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
index 37a112e..1dbbb84 100644
--- a/proxmox-frr/Cargo.toml
+++ b/proxmox-frr/Cargo.toml
@@ -17,7 +17,7 @@ serde = { workspace = true, features = [ "derive" ] }
serde_repr = "0.1"
minijinja = { version = "2.5", features = [ "multi_template", "loader" ] }
-proxmox-network-types = { workspace = true }
+proxmox-network-types = { workspace = true, features = ["api-types"] }
proxmox-sdn-types = { workspace = true }
proxmox-serde = { workspace = true }
proxmox-sortable-macro = "1"
diff --git a/proxmox-frr/src/ser/route_map.rs b/proxmox-frr/src/ser/route_map.rs
index d12ae05..22807f1 100644
--- a/proxmox-frr/src/ser/route_map.rs
+++ b/proxmox-frr/src/ser/route_map.rs
@@ -1,6 +1,11 @@
+use core::net::{Ipv4Addr, Ipv6Addr};
use std::net::IpAddr;
use proxmox_network_types::ip_address::Cidr;
+use proxmox_sdn_types::{
+ bgp::{EvpnRouteType, SetMetricValue, SetTagValue},
+ IntegerWithSign, Vni,
+};
use serde::{Deserialize, Serialize};
/// The action for a [`AccessListRule`].
@@ -45,6 +50,12 @@ impl AccessListName {
}
}
+impl PrefixListName {
+ pub fn new(name: String) -> PrefixListName {
+ PrefixListName(name)
+ }
+}
+
/// A FRR access-list.
///
/// Holds a vec of rules. Each rule will get its own line, FRR will collect all the rules with the
@@ -83,42 +94,36 @@ pub struct VniMatch {
/// execute its actions. If we match on an IP, there are two different syntaxes: `match ip ...` or
/// `match ipv6 ...`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(tag = "protocol_type")]
+#[serde(tag = "key", content = "value")]
pub enum RouteMapMatch {
- #[serde(rename = "ip")]
- V4(RouteMapMatchInner),
- #[serde(rename = "ipv6")]
- V6(RouteMapMatchInner),
- #[serde(rename = "vni")]
- Vni(u32),
-}
-
-#[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, Serialize, Deserialize)]
-#[serde(tag = "match_type", content = "value", rename_all = "kebab-case")]
-pub enum RouteMapMatchInner {
- 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),
+ #[serde(rename = "evpn route-type")]
+ RouteType(EvpnRouteType),
+ #[serde(rename = "evpn vni")]
+ Vni(Vni),
+ #[serde(rename = "ip address")]
+ IpAddressAccessList(AccessListName),
+ #[serde(rename = "ipv6 address")]
+ Ip6AddressAccessList(AccessListName),
+ #[serde(rename = "ip address prefix-list")]
+ IpAddressPrefixList(PrefixListName),
+ #[serde(rename = "ipv6 address prefix-list")]
+ Ip6AddressPrefixList(PrefixListName),
+ #[serde(rename = "ip next-hop prefix-list")]
+ IpNextHopPrefixList(PrefixListName),
+ #[serde(rename = "ipv6 next-hop prefix-list")]
+ Ip6NextHopPrefixList(PrefixListName),
+ #[serde(rename = "ip next-hop address")]
+ IpNextHopAddress(Ipv4Addr),
+ #[serde(rename = "ipv6 next-hop address")]
+ Ip6NextHopAddress(Ipv6Addr),
+ #[serde(rename = "metric")]
+ Metric(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ #[serde(rename = "local-preference")]
+ LocalPreference(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ #[serde(rename = "peer")]
+ Peer(String),
+ #[serde(rename = "tag")]
+ Tag(SetTagValue),
}
/// Defines the Action a route-map takes when it matches on a route.
@@ -126,12 +131,30 @@ pub enum SetTagValue {
/// 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, Serialize, Deserialize)]
-#[serde(tag = "set_type", content = "value", rename_all = "kebab-case")]
+#[serde(tag = "key", content = "value")]
pub enum RouteMapSet {
- LocalPreference(u32),
+ #[serde(rename = "ip next-hop peer-address")]
+ IpNextHopPeerAddress,
+ #[serde(rename = "ip next-hop unchanged")]
+ IpNextHopUnchanged,
+ #[serde(rename = "ip next-hop")]
+ IpNextHop(Ipv4Addr),
+ #[serde(rename = "ipv6 next-hop peer-address")]
+ Ip6NextHopPeerAddress,
+ #[serde(rename = "ipv6 next-hop prefer-global")]
+ Ip6NextHopPreferGlobal,
+ #[serde(rename = "ipv6 next-hop global")]
+ Ip6NextHop(Ipv6Addr),
+ #[serde(rename = "local-preference")]
+ LocalPreference(IntegerWithSign),
+ #[serde(rename = "tag")]
+ Tag(SetTagValue),
+ #[serde(rename = "weight")]
+ Weight(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ #[serde(rename = "metric")]
+ Metric(SetMetricValue),
+ #[serde(rename = "src")]
Src(IpAddr),
- Metric(u32),
- Community(String),
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-ve-rs 3/9] frr: allow rendering prefix-lists/route-maps separately
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (3 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 2/9] frr: implement routemap match/set statements via adjacent tagging Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 14:32 ` Gabriel Goller
2026-03-27 10:50 ` Hannes Laimer
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 4/9] frr-templates: change route maps template to adapt to new types Stefan Hanreich
` (23 subsequent siblings)
28 siblings, 2 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
This is mainly useful for integration tests for the route maps /
prefix lists feature in ve-config. It utilizes those methods to render
route maps and prefix lists in tests, without having to render a full
FRR configuration.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-frr/src/ser/serializer.rs | 35 ++++++++++++++++++++++++++++++-
1 file changed, 34 insertions(+), 1 deletion(-)
diff --git a/proxmox-frr/src/ser/serializer.rs b/proxmox-frr/src/ser/serializer.rs
index 2ac85d8..91aff1f 100644
--- a/proxmox-frr/src/ser/serializer.rs
+++ b/proxmox-frr/src/ser/serializer.rs
@@ -1,7 +1,12 @@
+use std::collections::BTreeMap;
+
use anyhow::Context;
use minijinja::Environment;
-use crate::ser::FrrConfig;
+use crate::ser::{
+ route_map::{PrefixListName, PrefixListRule, RouteMapEntry, RouteMapName},
+ FrrConfig,
+};
use proxmox_sortable_macro::sortable;
#[sortable]
@@ -91,3 +96,31 @@ pub fn dump(config: &FrrConfig) -> Result<String, anyhow::Error> {
pub fn to_raw_config(config: &FrrConfig) -> Result<Vec<String>, anyhow::Error> {
Ok(dump(config)?.lines().map(|line| line.to_owned()).collect())
}
+
+/// Render Prefix Lists into an FRR config string.
+///
+/// Currently only used for testing the rendering of prefix lists, to avoid having to render the
+/// whole FRR configuration file.
+pub fn render_prefix_lists(
+ prefix_lists: BTreeMap<PrefixListName, Vec<PrefixListRule>>,
+) -> Result<String, anyhow::Error> {
+ create_env()
+ .get_template("prefix_lists.jinja")
+ .with_context(|| "could not obtain frr template from environment")?
+ .render(prefix_lists)
+ .with_context(|| "could not render frr template")
+}
+
+/// Render Route Maps into an FRR config string.
+///
+/// Currently only used for testing the rendering of route maps, to avoid having to render the
+/// whole FRR configuration file.
+pub fn render_route_maps(
+ route_maps: BTreeMap<RouteMapName, Vec<RouteMapEntry>>,
+) -> Result<String, anyhow::Error> {
+ create_env()
+ .get_template("route_maps.jinja")
+ .with_context(|| "could not obtain frr template from environment")?
+ .render(route_maps)
+ .with_context(|| "could not render frr template")
+}
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-ve-rs 4/9] frr-templates: change route maps template to adapt to new types
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (4 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 3/9] frr: allow rendering prefix-lists/route-maps separately Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 14:33 ` Gabriel Goller
2026-03-27 11:01 ` Hannes Laimer
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 5/9] ve-config: add prefix list section config Stefan Hanreich
` (22 subsequent siblings)
28 siblings, 2 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Instead of defining every potential match / set type manually under a
different name, proxmox-frr now uses the Adjacently tagged
representation for representing key/value pairs for match/set actions.
This allows simplifying the route_maps template by simply rendering
the respective key / value fields.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-frr-templates/templates/route_maps.jinja | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/proxmox-frr-templates/templates/route_maps.jinja b/proxmox-frr-templates/templates/route_maps.jinja
index 172c682..19a6ee4 100644
--- a/proxmox-frr-templates/templates/route_maps.jinja
+++ b/proxmox-frr-templates/templates/route_maps.jinja
@@ -3,16 +3,12 @@
!
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 }}
-{% elif match.match_type == "next-hop" %}
- match {{ match.protocol_type }} next-hop {{ match.value }}
-{% endif %}
+ match {{ match.key }} {% if match.value is defined %} {{ match.value }} {% endif %}
+
{% endfor %}
{% for set in routemap.sets %}
- set {{ set.set_type }} {{ set.value }}
+ set {{ set.key }} {% if set.value is defined %} {{ set.value }} {% endif %}
+
{% endfor %}
{% for line in routemap.custom_frr_config %}
{{ line }}
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-ve-rs 5/9] ve-config: add prefix list section config
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (5 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 4/9] frr-templates: change route maps template to adapt to new types Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 6/9] ve-config: frr: implement frr config generation for prefix lists Stefan Hanreich
` (21 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Those types represent FRR prefix lists inside a section config format.
For an example of the exact format and its FRR representation see the
module-level documentation.
Contrary to most SDN entities, prefix list IDs can be 32 characters
long instead of 8 and support underscores as well as hyphens. This is
because the restriction of having to generate network interface names
does not apply to FRR entities, so we can be more lenient with IDs
here, allowing users to specify more descriptive names.
There is support for specifying the sequence number of the prefix list
entry, but it will be backend-only. FRR supports auto-generating the
sequence numbers, so it is sufficient to write them to the FRR
configuration in the desired order.
The API types are currently equivalent to the section config types,
but are still contained as type aliases in their own module, to make
changing that in the future easier, since it requires no changes to
call sites as long as the exposed API of the struct is the same.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/mod.rs | 1 +
proxmox-ve-config/src/sdn/prefix_list.rs | 160 +++++++++++++++++++++++
2 files changed, 161 insertions(+)
create mode 100644 proxmox-ve-config/src/sdn/prefix_list.rs
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 86dcd93..344c02c 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -1,6 +1,7 @@
pub mod config;
pub mod fabric;
pub mod ipam;
+pub mod prefix_list;
use std::{error::Error, fmt::Display, str::FromStr};
diff --git a/proxmox-ve-config/src/sdn/prefix_list.rs b/proxmox-ve-config/src/sdn/prefix_list.rs
new file mode 100644
index 0000000..f4988d9
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/prefix_list.rs
@@ -0,0 +1,160 @@
+//! Section config types for FRR Prefix Lists.
+//!
+//! This module contains the API types for representing FRR Prefix Lists as section config. Each
+//! entry in the section config represents a Prefix List and its entries.
+//!
+//! A simple FRR Prefix List looks like this:
+//!
+//! ```text
+//! ip prefix-list example-list permit 192.0.2.0/24 ge 25 le 26
+//! ip prefix-list example-list permit 192.0.2.0/24 le 28 ge 29
+//! ip prefix-list example-list deny 192.0.2.0/24 le 24
+//! ```
+//!
+//! The corresponding section config entry looks like this:
+//!
+//! ```text
+//! prefix-list: example_list
+//! entries action=permit,prefix=192.0.2.0/24,ge=25,le=26
+//! entries action=permit,prefix=192.0.2.0/24,ge=28,le=29
+//! entries action=deny,prefix=192.0.2.0/24,le=24
+//! ```
+
+use const_format::concatcp;
+use serde::{Deserialize, Serialize};
+
+use proxmox_network_types::Cidr;
+use proxmox_schema::{
+ api, api_string_type, const_regex, property_string::PropertyString, ApiStringFormat, Updater,
+ UpdaterType,
+};
+
+pub const PREFIX_LIST_ID_REGEX_STR: &str =
+ r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-_]){0,30}(?:[a-zA-Z0-9]){0,1})";
+
+const_regex! {
+ pub PREFIX_LIST_ID_REGEX = concatcp!(r"^", PREFIX_LIST_ID_REGEX_STR, r"$");
+}
+
+pub const PREFIX_LIST_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&PREFIX_LIST_ID_REGEX);
+
+api_string_type! {
+ /// ID of a Prefix List.
+ #[api(format: &PREFIX_LIST_ID_FORMAT)]
+ #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, UpdaterType)]
+ pub struct PrefixListId(String);
+}
+
+#[api()]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+/// Action for an entry in a Prefix List.
+pub enum PrefixListAction {
+ /// permit
+ Permit,
+ /// deny
+ Deny,
+}
+
+#[api(
+ properties: {
+ entries: {
+ type: Array,
+ optional: true,
+ items: {
+ type: String,
+ description: "An entry in a prefix list",
+ format: &ApiStringFormat::PropertyString(&PrefixListEntry::API_SCHEMA),
+ }
+ },
+ }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater)]
+/// IP Prefix List
+///
+/// Corresponds to the FRR IP Prefix lists, as described in its [documentation](https://docs.frrouting.org/en/latest/filter.html#ip-prefix-list)
+pub struct PrefixListSection {
+ #[updater(skip)]
+ id: PrefixListId,
+ /// The entries in this prefix list
+ pub entries: Vec<PropertyString<PrefixListEntry>>,
+}
+
+impl PrefixListSection {
+ /// Return the ID of the Prefix List.
+ pub fn id(&self) -> &PrefixListId {
+ &self.id
+ }
+}
+
+#[api()]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+/// IP Prefix List Entry
+///
+/// Corresponds to the FRR IP Prefix lists, as described in its [documentation](https://docs.frrouting.org/en/latest/filter.html#ip-prefix-list)
+pub struct PrefixListEntry {
+ action: PrefixListAction,
+ prefix: Cidr,
+ /// Prefix length - entry will be applied if the prefix length is less than or equal to this
+ /// value.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ le: Option<u32>,
+ /// Prefix length - entry will be applied if the prefix length is greater than or equal to this
+ /// value.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ ge: Option<u32>,
+ /// The sequence number for this prefix list entry.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ seq: Option<u32>,
+}
+
+#[api(
+ "id-property": "id",
+ "id-schema": {
+ type: String,
+ description: "Prefix List Section ID",
+ format: &PREFIX_LIST_ID_FORMAT,
+ },
+ "type-key": "type",
+)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", tag = "type")]
+pub enum PrefixList {
+ PrefixList(PrefixListSection),
+}
+
+pub mod api {
+ use super::*;
+
+ pub type PrefixList = PrefixListSection;
+ pub type PrefixListUpdater = PrefixListSectionUpdater;
+
+ #[derive(Debug, Clone, Serialize, Deserialize)]
+ #[serde(rename_all = "kebab-case")]
+ /// Deletable properties for [`PrefixList`].
+ pub enum PrefixListDeletableProperties {
+ Entries,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use proxmox_section_config::typed::ApiSectionDataEntry;
+
+ use super::*;
+
+ #[test]
+ fn test_simple_prefix_list() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+prefix-list: somelist
+ entries action=permit,prefix=192.0.2.0/24
+ entries action=permit,prefix=192.0.2.0/24,le=32
+ entries action=permit,prefix=192.0.2.0/24,le=32,ge=24,seq=123
+ entries action=permit,prefix=192.0.2.0/24,ge=24
+ entries action=permit,prefix=192.0.2.0/24,ge=24,le=31
+"#;
+
+ PrefixList::parse_section_config("prefix-lists.cfg", section_config)?;
+ Ok(())
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-ve-rs 6/9] ve-config: frr: implement frr config generation for prefix lists
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (6 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 5/9] ve-config: add prefix list section config Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 7/9] ve-config: add route map section config Stefan Hanreich
` (20 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Implements conversion traits for all the section config types, so they
can be converted into their respective FRR template counterpart.
Also add a helper that adds a list of prefix lists to an existing FRR
configuration. This will be used by perl-rs to generate the FRR
configuration from the section configuration. The helper will
overwrite existing prefix lists in the FRR configuration, allowing
users to override pre-defined prefix lists generated by our stack.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/prefix_list.rs | 187 +++++++++++++++++++++++
1 file changed, 187 insertions(+)
diff --git a/proxmox-ve-config/src/sdn/prefix_list.rs b/proxmox-ve-config/src/sdn/prefix_list.rs
index f4988d9..f371c8d 100644
--- a/proxmox-ve-config/src/sdn/prefix_list.rs
+++ b/proxmox-ve-config/src/sdn/prefix_list.rs
@@ -123,6 +123,193 @@ pub enum PrefixList {
PrefixList(PrefixListSection),
}
+#[cfg(feature = "frr")]
+pub mod frr {
+ use core::{convert::Into, iter::IntoIterator};
+
+ use super::*;
+
+ use proxmox_frr::ser::{
+ route_map::{
+ self, PrefixList as FrrPrefixList, PrefixListName, PrefixListRule as FrrPrefixListRule,
+ },
+ FrrConfig,
+ };
+
+ impl Into<PrefixListName> for PrefixListId {
+ fn into(self) -> PrefixListName {
+ PrefixListName::new(self.into_string())
+ }
+ }
+
+ impl Into<FrrPrefixListRule> for PrefixListEntry {
+ fn into(self) -> FrrPrefixListRule {
+ FrrPrefixListRule {
+ action: match self.action {
+ PrefixListAction::Permit => route_map::AccessAction::Permit,
+ PrefixListAction::Deny => route_map::AccessAction::Deny,
+ },
+ network: self.prefix,
+ seq: self.seq,
+ le: self.le,
+ ge: self.ge,
+ is_ipv6: self.prefix.is_ipv6(),
+ }
+ }
+ }
+
+ impl Into<FrrPrefixList> for PrefixListSection {
+ fn into(self) -> FrrPrefixList {
+ FrrPrefixList {
+ name: PrefixListName::new(self.id.to_string()),
+ rules: self
+ .entries
+ .into_iter()
+ .map(|rule| rule.into_inner().into())
+ .collect(),
+ }
+ }
+ }
+
+ /// Add a list of Prefix Lists to an [`FrrConfig`].
+ ///
+ /// This will overwrite existing Prefix Lists in the [`FrrConfig`]. Since this will be used for
+ /// generating the FRR configuration from the SDN stack, this enables users to override Prefix
+ /// Lists that are predefined by our stack.
+ pub fn build_frr_prefix_lists(
+ prefix_lists: impl IntoIterator<Item = PrefixList>,
+ frr_config: &mut FrrConfig,
+ ) -> Result<(), anyhow::Error> {
+ for prefix_list in prefix_lists.into_iter() {
+ let PrefixList::PrefixList(prefix_list) = prefix_list;
+ let prefix_list_name = PrefixListName::new(prefix_list.id.0);
+
+ frr_config.prefix_lists.insert(
+ prefix_list_name,
+ prefix_list
+ .entries
+ .into_iter()
+ .map(|prefix_list| prefix_list.into_inner().into())
+ .collect(),
+ );
+ }
+
+ Ok(())
+ }
+
+ #[cfg(test)]
+ mod tests {
+ use super::*;
+
+ use proxmox_frr::ser::route_map::{AccessAction, PrefixListName};
+ use proxmox_frr::ser::serializer::dump;
+
+ use proxmox_section_config::typed::ApiSectionDataEntry;
+
+ #[test]
+ fn test_build_prefix_list() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+prefix-list: example-1
+ entries action=permit,prefix=192.0.2.0/24
+ entries action=permit,prefix=192.0.2.0/24,le=32
+ entries action=permit,prefix=192.0.2.0/24,le=32,ge=24,seq=123
+ entries action=permit,prefix=192.0.2.0/24,ge=24
+ entries action=permit,prefix=192.0.2.0/24,ge=24,le=31
+
+prefix-list: example-3
+ entries action=permit,prefix=192.0.2.0/24,seq=333
+ entries action=permit,prefix=198.51.100.0/24,seq=222
+ entries action=permit,prefix=203.0.113.0/24,seq=111
+
+prefix-list: example-2
+ entries action=deny,prefix=192.0.2.0/24,le=25
+ entries action=permit,prefix=192.0.2.0/24
+"#;
+
+ let config = PrefixList::parse_section_config("prefix-lists.cfg", section_config)?;
+ let mut frr_config = FrrConfig::default();
+
+ build_frr_prefix_lists(
+ config
+ .into_iter()
+ .map(|(_, route_map_entry)| route_map_entry),
+ &mut frr_config,
+ )?;
+
+ assert_eq!(
+ dump(&frr_config)?,
+ r#"!
+ip prefix-list example-1 permit 192.0.2.0/24
+ip prefix-list example-1 permit 192.0.2.0/24 le 32
+ip prefix-list example-1 seq 123 permit 192.0.2.0/24 le 32 ge 24
+ip prefix-list example-1 permit 192.0.2.0/24 ge 24
+ip prefix-list example-1 permit 192.0.2.0/24 le 31 ge 24
+!
+ip prefix-list example-2 deny 192.0.2.0/24 le 25
+ip prefix-list example-2 permit 192.0.2.0/24
+!
+ip prefix-list example-3 seq 333 permit 192.0.2.0/24
+ip prefix-list example-3 seq 222 permit 198.51.100.0/24
+ip prefix-list example-3 seq 111 permit 203.0.113.0/24
+"#
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_build_prefix_list_overwrite() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+prefix-list: example-1
+ entries action=permit,prefix=192.0.2.0/24
+"#;
+
+ let config = PrefixList::parse_section_config("prefix-lists.cfg", section_config)?;
+
+ let example_1_prefix_list = vec![FrrPrefixListRule {
+ action: AccessAction::Deny,
+ network: Cidr::new_v4([198, 51, 100, 0], 24).unwrap(),
+ seq: None,
+ le: None,
+ ge: None,
+ is_ipv6: false,
+ }];
+
+ let mut frr_config = FrrConfig::default();
+
+ frr_config.prefix_lists.insert(
+ PrefixListName::new("example-1".to_string()),
+ example_1_prefix_list.clone(),
+ );
+
+ build_frr_prefix_lists(
+ config
+ .into_iter()
+ .map(|(_, route_map_entry)| route_map_entry),
+ &mut frr_config,
+ )?;
+
+ let new_prefix_list = frr_config
+ .prefix_lists
+ .get(&PrefixListName::new("example-1".to_string()))
+ .expect("'example-1' prefix list exists");
+
+ assert_ne!(&example_1_prefix_list, new_prefix_list);
+
+ let generated_frr_config = dump(&frr_config)?;
+
+ assert_eq!(
+ generated_frr_config,
+ r#"!
+ip prefix-list example-1 permit 192.0.2.0/24
+"#
+ );
+
+ Ok(())
+ }
+ }
+}
+
pub mod api {
use super::*;
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-ve-rs 7/9] ve-config: add route map section config
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (7 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 6/9] ve-config: frr: implement frr config generation for prefix lists Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 14:35 ` Gabriel Goller
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 8/9] ve-config: frr: implement frr config generation for route maps Stefan Hanreich
` (19 subsequent siblings)
28 siblings, 1 reply; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Those types represent FRR route maps inside a section config format.
For an example of the exact format and its FRR representation see the
module-level documentation.
One section config entry maps to one route map entry. A route map
consists of one or more route map entries inside the section config.
The ID of a section encodes the name of the route map as well as the
order # of the entry.
The route map module exports specific types for the API that handle
converting the section config ID, because currently it is only
possible to deserialize section config IDs to Strings. To avoid
having to implement the parsing logic along every step of the stack
(Perl backend, UI), use specific API types in the public API that
handle parsing the section ID into route map name and order.
Contrary to most SDN entities, route maps IDs can be 32 characters
long instead of 8 and support underscores as well as hyphens. This is
because the restriction of having to generate network interface names
does not apply to FRR entities, so we can be more lenient with IDs
here, allowing users to specify more descriptive names.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/debian/control | 2 +
proxmox-ve-config/src/sdn/mod.rs | 1 +
proxmox-ve-config/src/sdn/route_map.rs | 491 +++++++++++++++++++++++++
3 files changed, 494 insertions(+)
create mode 100644 proxmox-ve-config/src/sdn/route_map.rs
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
index 440cf73..5206340 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -24,6 +24,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>,
+ librust-serde-with-3+default-dev <!nocheck>,
librust-thiserror-2+default-dev <!nocheck>,
librust-tracing-0.1+default-dev (>= 0.1.37-~~) <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
@@ -55,6 +56,7 @@ Depends:
librust-serde-1+default-dev,
librust-serde-1+derive-dev,
librust-serde-json-1+default-dev,
+ librust-serde-with-3+default-dev,
librust-thiserror-2+default-dev,
librust-tracing-0.1+default-dev (>= 0.1.37-~~)
Suggests:
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 344c02c..24069ad 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -2,6 +2,7 @@ pub mod config;
pub mod fabric;
pub mod ipam;
pub mod prefix_list;
+pub mod route_map;
use std::{error::Error, fmt::Display, str::FromStr};
diff --git a/proxmox-ve-config/src/sdn/route_map.rs b/proxmox-ve-config/src/sdn/route_map.rs
new file mode 100644
index 0000000..3f4da56
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/route_map.rs
@@ -0,0 +1,491 @@
+//! Section config types for FRR Route Maps.
+//!
+//! This module contains the API types required for representing FRR Route Maps as section config.
+//! Each entry in the section config maps to a Route Map entry, *not* a route map as a whole, the
+//! order of the entry is encoded in the ID of the Route Map.
+//!
+//! Route maps in FRR consists of at least one entry, which are ordered by their given sequence
+//! number / order. Each entry has a default matching policy, which is applied if the matching
+//! conditions of the entry are met.
+//!
+//! An example for a simple FRR Route Map entry loooks like this:
+//!
+//! ```text
+//! route-map test permit 10
+//! match ip next-hop address 192.0.2.1
+//! set local-preference 200
+//! ```
+//!
+//! The corresponding representation as a section config entry looks like this:
+//!
+//! ```text
+//! route-map-entry: test_10
+//! action permit
+//! match key=ip-next-hop-address,value=192.0.2.1
+//! set key=local-preference,value=200
+//! ```
+//!
+//! Match and Set Actions are encoded as an array with a property string that has a key and an
+//! optional value paramter, because some options do not require an additional value.
+//!
+//! This abstraction currently supports Match and Set actions, but not call actions and exit
+//! actions.
+
+use core::net::IpAddr;
+
+use anyhow::format_err;
+use const_format::concatcp;
+
+use proxmox_network_types::ip_address::api_types::{Ipv4Addr, Ipv6Addr};
+use proxmox_sdn_types::{
+ bgp::{EvpnRouteType, SetMetricValue, SetTagValue},
+ IntegerWithSign, Vni,
+};
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{
+ api, api_string_type, const_regex, property_string::PropertyString, ApiStringFormat, ApiType,
+ EnumEntry, ObjectSchema, Schema, StringSchema, Updater, UpdaterType,
+};
+
+use crate::sdn::prefix_list::PrefixListId;
+
+pub const ROUTE_MAP_ID_REGEX_STR: &str =
+ r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-_]){0,30}(?:[a-zA-Z0-9]){0,1})";
+
+pub const ROUTE_MAP_ORDER_REGEX_STR: &str = r"\d+";
+
+const_regex! {
+ pub ROUTE_MAP_ID_REGEX = concatcp!(r"^", ROUTE_MAP_ID_REGEX_STR, r"$");
+ pub ROUTE_MAP_SECTION_ID_REGEX = concatcp!(r"^", ROUTE_MAP_ID_REGEX_STR, r"_", ROUTE_MAP_ORDER_REGEX_STR, r"$");
+}
+
+pub const ROUTE_MAP_SECTION_ID_FORMAT: ApiStringFormat =
+ ApiStringFormat::Pattern(&ROUTE_MAP_SECTION_ID_REGEX);
+
+pub const ROUTE_MAP_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&ROUTE_MAP_ID_REGEX);
+
+api_string_type! {
+ /// ID of a Route Map..
+ #[api(format: &ROUTE_MAP_ID_FORMAT)]
+ #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, UpdaterType)]
+ pub struct RouteMapId(String);
+}
+
+/// The ID of a Route Map entry in the section config (name + order).
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RouteMapEntryId {
+ /// name of the Route Map
+ route_map_id: RouteMapId,
+ /// seq nr of the Route Map
+ order: u32,
+}
+
+impl RouteMapEntryId {
+ /// Create a new Route Map Entry ID.
+ pub fn new(route_map_id: RouteMapId, order: u32) -> Self {
+ Self {
+ route_map_id,
+ order,
+ }
+ }
+
+ /// Returns the name part of the Route Map section id.
+ pub fn route_map_id(&self) -> &RouteMapId {
+ &self.route_map_id
+ }
+
+ /// Returns the order part of the Route Map section id.
+ pub fn order(&self) -> u32 {
+ self.order
+ }
+}
+
+impl ApiType for RouteMapEntryId {
+ const API_SCHEMA: Schema = StringSchema::new("ID of a SDN node in the section config")
+ .format(&ROUTE_MAP_SECTION_ID_FORMAT)
+ .schema();
+}
+
+impl std::fmt::Display for RouteMapEntryId {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}_{}", self.route_map_id, self.order)
+ }
+}
+
+proxmox_serde::forward_serialize_to_display!(RouteMapEntryId);
+
+impl std::str::FromStr for RouteMapEntryId {
+ type Err = anyhow::Error;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ let (name, order) = value
+ .rsplit_once("_")
+ .ok_or_else(|| format_err!("invalid RouteMap section id: {}", value))?;
+
+ Ok(Self {
+ route_map_id: RouteMapId::from_string(name.to_string())?,
+ order: order.parse()?,
+ })
+ }
+}
+
+proxmox_serde::forward_deserialize_to_from_str!(RouteMapEntryId);
+
+#[api(
+ "id-property": "id",
+ "id-schema": {
+ type: String,
+ description: "Route Map Section ID",
+ format: &ROUTE_MAP_SECTION_ID_FORMAT,
+ },
+ "type-key": "type",
+)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", tag = "type")]
+/// The Route Map section config type.
+pub enum RouteMap {
+ RouteMapEntry(RouteMapEntry),
+}
+
+#[api()]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+/// Matching policy of a Route Map entry.
+pub enum RouteMapAction {
+ /// Permit
+ Permit,
+ /// Deny
+ Deny,
+}
+
+#[api(
+ properties: {
+ set: {
+ type: Array,
+ description: "A list of Set actions to perform in this entry.",
+ optional: true,
+ items: {
+ type: String,
+ description: "A specific Set action.",
+ format: &ApiStringFormat::PropertyString(&SetAction::API_SCHEMA),
+ }
+ },
+ "match": {
+ type: Array,
+ description: "A list of Match actions to perform in this entry.",
+ optional: true,
+ items: {
+ type: String,
+ description: "A specific match action.",
+ format: &ApiStringFormat::PropertyString(&MatchAction::API_SCHEMA),
+ }
+ },
+ }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+/// Route Map Entry
+///
+/// Represents one entry in a Route Map. One Route Map is made up of one or more entries, that are
+/// executed in order of their ordering number.
+pub struct RouteMapEntry {
+ id: RouteMapEntryId,
+ action: RouteMapAction,
+ #[serde(default, rename = "set")]
+ set_actions: Vec<PropertyString<SetAction>>,
+ #[serde(default, rename = "match")]
+ match_actions: Vec<PropertyString<MatchAction>>,
+}
+
+impl RouteMapEntry {
+ /// Return the ID of the Route Map.
+ pub fn id(&self) -> &RouteMapEntryId {
+ &self.id
+ }
+
+ /// Sets the action for this entry.
+ pub fn set_action(&mut self, action: RouteMapAction) {
+ self.action = action;
+ }
+
+ /// Set the set actions for this route map entry.
+ pub fn set_set_actions(
+ &mut self,
+ set_actions: impl IntoIterator<Item = PropertyString<SetAction>>,
+ ) {
+ self.set_actions = set_actions.into_iter().collect();
+ }
+
+ /// Set the match actions for this route map entry.
+ pub fn set_match_actions(
+ &mut self,
+ match_actions: impl IntoIterator<Item = PropertyString<MatchAction>>,
+ ) {
+ self.match_actions = match_actions.into_iter().collect();
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", tag = "key", content = "value")]
+/// A Route Map set action
+pub enum SetAction {
+ IpNextHopPeerAddress,
+ IpNextHopUnchanged,
+ IpNextHop(Ipv4Addr),
+ Ip6NextHopPeerAddress,
+ Ip6NextHopPreferGlobal,
+ Ip6NextHop(Ipv6Addr),
+ Weight(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ Tag(SetTagValue),
+ Metric(SetMetricValue),
+ LocalPreference(IntegerWithSign),
+ Src(IpAddr),
+}
+
+impl ApiType for SetAction {
+ const API_SCHEMA: Schema = ObjectSchema::new(
+ "FRR set action",
+ &[
+ (
+ "key",
+ false,
+ &StringSchema::new("The key indicating which value should be set.")
+ .format(&ApiStringFormat::Enum(&[
+ EnumEntry::new(
+ "ip-next-hop-peer-address",
+ "Sets the BGP nexthop address to the IPv4 peer address.",
+ ),
+ EnumEntry::new("ip-next-hop-unchanged", "Leaves the nexthop unchanged."),
+ EnumEntry::new(
+ "ip-next-hop",
+ "Sets the nexthop to the given IPv4 address.",
+ ),
+ EnumEntry::new(
+ "ip6-next-hop-peer-address",
+ "Sets the BGP nexthop address to the IPv6 peer address.",
+ ),
+ EnumEntry::new(
+ "ip6-next-hop-prefer-global",
+ "If a LLA and GUA are received, prefer the GUA.",
+ ),
+ EnumEntry::new(
+ "ip6-next-hop",
+ "Sets the nexthop to the given IPv6 address.",
+ ),
+ EnumEntry::new(
+ "local-preference",
+ "Sets the local preference for this route.",
+ ),
+ EnumEntry::new("tag", "Sets a tag for the route."),
+ EnumEntry::new("weight", "Sets the weight for the route."),
+ EnumEntry::new("metric", "Sets the metric for the route."),
+ EnumEntry::new(
+ "src",
+ "The source address to insert into the kernel routing table.",
+ ),
+ ]))
+ .schema(),
+ ),
+ (
+ "value",
+ true,
+ &StringSchema::new("The value that should be set - depends on the given key.")
+ .schema(),
+ ),
+ ],
+ )
+ .schema();
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", tag = "key", content = "value")]
+pub enum MatchAction {
+ RouteType(EvpnRouteType),
+ Vni(Vni),
+ IpAddressPrefixList(PrefixListId),
+ Ip6AddressPrefixList(PrefixListId),
+ IpNextHopPrefixList(PrefixListId),
+ Ip6NextHopPrefixList(PrefixListId),
+ IpNextHopAddress(Ipv4Addr),
+ Ip6NextHopAddress(Ipv6Addr),
+ Tag(SetTagValue),
+ Metric(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ LocalPreference(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32),
+ Peer(String),
+}
+
+impl ApiType for MatchAction {
+ const API_SCHEMA: Schema = ObjectSchema::new(
+ "FRR set action",
+ &[
+ (
+ "key",
+ false,
+ &StringSchema::new("The key indicating on which value to match.")
+ .format(&ApiStringFormat::Enum(&[
+ EnumEntry::new("route-type", "Match the EVPN route type."),
+ EnumEntry::new("vni", "Match the VNI of an EVPN route."),
+ EnumEntry::new(
+ "ip-address-prefix-list",
+ "Match the IPv4 CIDR to a prefix-list.",
+ ),
+ EnumEntry::new(
+ "ip6-address-prefix-list",
+ "Match the IPv6 CIDR to a prefix-list",
+ ),
+ EnumEntry::new(
+ "ip-next-hop-prefix-list",
+ "Match the IPv4 next-hop to a prefix-list.",
+ ),
+ EnumEntry::new(
+ "ip6-next-hop-prefix-list",
+ "Match the IPv4 next-hop to a prefix-list.",
+ ),
+ EnumEntry::new(
+ "ip-next-hop-address",
+ "Match the next-hop to an IPv4 address.",
+ ),
+ EnumEntry::new(
+ "ip6-next-hop-address",
+ "Match the next-hop to an IPv6 address.",
+ ),
+ EnumEntry::new("metric", "Match the metric of the route."),
+ EnumEntry::new("local-preference", "Match the local preference."),
+ EnumEntry::new(
+ "peer",
+ "Match the peer IP address, interface name or peer group.",
+ ),
+ ]))
+ .schema(),
+ ),
+ (
+ "value",
+ true,
+ &StringSchema::new("The value that should be matched - depends on the given key.")
+ .schema(),
+ ),
+ ],
+ )
+ .schema();
+}
+
+pub mod api {
+ //! API type for Route Map Entries.
+ //!
+ //! Since Route Map Entries encode information in their ID, these types help converting to /
+ //! from the Section Config types.
+ use super::*;
+
+ #[api(
+ properties: {
+ set: {
+ type: Array,
+ description: "A list of set actions for this Route Map entry",
+ optional: true,
+ items: {
+ type: String,
+ description: "A set action",
+ format: &ApiStringFormat::PropertyString(&SetAction::API_SCHEMA),
+ }
+ },
+ "match": {
+ type: Array,
+ description: "A list of match actions for this Route Map entry",
+ optional: true,
+ items: {
+ type: String,
+ description: "A match action",
+ format: &ApiStringFormat::PropertyString(&MatchAction::API_SCHEMA),
+ }
+ },
+ }
+ )]
+ #[derive(Debug, Clone, Serialize, Deserialize, Updater)]
+ #[serde(rename_all = "kebab-case")]
+ /// Route Map entry
+ pub struct RouteMapEntry {
+ /// name of the Route Map
+ #[updater(skip)]
+ pub route_map_id: RouteMapId,
+ /// seq nr of the Route Map
+ #[updater(skip)]
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")]
+ pub order: u32,
+ pub action: RouteMapAction,
+ #[serde(default, rename = "set")]
+ pub set_actions: Vec<PropertyString<SetAction>>,
+ #[serde(default, rename = "match")]
+ pub match_actions: Vec<PropertyString<MatchAction>>,
+ }
+
+ impl RouteMapEntry {
+ /// Return the ID of the Route Map this entry belongs to.
+ pub fn route_map_id(&self) -> &RouteMapId {
+ &self.route_map_id
+ }
+
+ /// Return the order for this Route Map entry.
+ pub fn order(&self) -> u32 {
+ self.order
+ }
+ }
+
+ #[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+ #[serde(rename_all = "kebab-case")]
+ /// Deletable properties for Route Map entries.
+ pub enum RouteMapDeletableProperties {
+ SetActions,
+ MatchActions,
+ }
+
+ impl From<super::RouteMapEntry> for RouteMapEntry {
+ fn from(value: super::RouteMapEntry) -> RouteMapEntry {
+ RouteMapEntry {
+ route_map_id: value.id.route_map_id,
+ order: value.id.order,
+ action: value.action,
+ set_actions: value.set_actions,
+ match_actions: value.match_actions,
+ }
+ }
+ }
+
+ impl From<RouteMapEntry> for super::RouteMapEntry {
+ fn from(value: RouteMapEntry) -> super::RouteMapEntry {
+ super::RouteMapEntry {
+ id: RouteMapEntryId {
+ route_map_id: value.route_map_id,
+ order: value.order,
+ },
+ action: value.action,
+ set_actions: value.set_actions,
+ match_actions: value.match_actions,
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use proxmox_section_config::typed::ApiSectionDataEntry;
+
+ use super::*;
+
+ #[test]
+ fn test_simple_route_map() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+route-map-entry: test_underscore_123
+ action permit
+ set key=tag,value=23487
+ set key=tag,value=untagged
+ set key=metric,value=+rtt
+ set key=local-preference,value=-12345
+ set key=ip-next-hop,value=192.0.2.0
+ match key=vni,value=23487
+ match key=vni,value=23487
+"#;
+
+ RouteMap::parse_section_config("route-maps.cfg", section_config)?;
+ Ok(())
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-ve-rs 8/9] ve-config: frr: implement frr config generation for route maps
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (8 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 7/9] ve-config: add route map section config Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 15:03 ` Gabriel Goller
2026-03-27 11:17 ` Hannes Laimer
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 9/9] ve-config: fabrics: adapt frr config generation to new format Stefan Hanreich
` (18 subsequent siblings)
28 siblings, 2 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Implements conversion traits for all the section config types, so they
can be converted into their respective FRR template counterpart.
This module contains a helper for adding all route map entries to an
existing FRR configuration. It will overwrite existing route map
entries that have the same name AND order number. But if entries with
the same name, but different ordering, exist they will only be added
to the existing FRR configuration without dropping the other route map
entries.
This currently not relevant either way, because the initial API
implementation will reject creating route maps with names of route
maps that the stack auto-generates. In the future this behavior can
be used for selectively overriding / appending existing Proxmox VE
route maps.
The helper also automatically orders route map entries according to
their ordering number. This allows for deterministic FRR configuration
output, which is important for tests and convenient for human
readability.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/route_map.rs | 271 +++++++++++++++++++++++++
1 file changed, 271 insertions(+)
diff --git a/proxmox-ve-config/src/sdn/route_map.rs b/proxmox-ve-config/src/sdn/route_map.rs
index 3f4da56..8d8c4dc 100644
--- a/proxmox-ve-config/src/sdn/route_map.rs
+++ b/proxmox-ve-config/src/sdn/route_map.rs
@@ -369,6 +369,277 @@ impl ApiType for MatchAction {
.schema();
}
+#[cfg(feature = "frr")]
+pub mod frr {
+ //! Route Map Entry FRR types
+ //!
+ //! This module contains implementations of conversion traits for the section config types, so
+ //! they can be converted to the respective proxmox-frr types. This enables easy conversion to
+ //! the proxmox-frr types and makes it possible to generate the FRR configuration for the Route
+ //! Map entries.
+ use core::{convert::Into, iter::IntoIterator};
+
+ use super::*;
+
+ use proxmox_frr::ser::{
+ route_map::{RouteMapEntry as FrrRouteMap, RouteMapMatch, RouteMapName, RouteMapSet},
+ FrrConfig,
+ };
+
+ use crate::sdn::route_map::RouteMapAction;
+
+ impl Into<RouteMapMatch> for MatchAction {
+ fn into(self) -> RouteMapMatch {
+ match self {
+ Self::RouteType(evpn_route_type) => RouteMapMatch::RouteType(evpn_route_type),
+ Self::Vni(vni) => RouteMapMatch::Vni(vni),
+ Self::IpAddressPrefixList(prefix_list_name) => {
+ RouteMapMatch::IpAddressPrefixList(prefix_list_name.into())
+ }
+ Self::Ip6AddressPrefixList(prefix_list_name) => {
+ RouteMapMatch::Ip6AddressPrefixList(prefix_list_name.into())
+ }
+ Self::IpNextHopPrefixList(prefix_list_name) => {
+ RouteMapMatch::IpNextHopPrefixList(prefix_list_name.into())
+ }
+ Self::Ip6NextHopPrefixList(prefix_list_name) => {
+ RouteMapMatch::Ip6NextHopPrefixList(prefix_list_name.into())
+ }
+ Self::IpNextHopAddress(ipv4_addr) => RouteMapMatch::IpNextHopAddress(*ipv4_addr),
+ Self::Ip6NextHopAddress(ipv6_addr) => RouteMapMatch::Ip6NextHopAddress(*ipv6_addr),
+ Self::Metric(metric) => RouteMapMatch::Metric(metric),
+ Self::LocalPreference(local_preference) => {
+ RouteMapMatch::LocalPreference(local_preference)
+ }
+ Self::Peer(ip_addr) => RouteMapMatch::Peer(ip_addr),
+ Self::Tag(tag) => RouteMapMatch::Tag(tag),
+ }
+ }
+ }
+
+ impl Into<RouteMapSet> for SetAction {
+ fn into(self) -> RouteMapSet {
+ match self {
+ Self::IpNextHopPeerAddress => RouteMapSet::IpNextHopPeerAddress,
+ Self::IpNextHopUnchanged => RouteMapSet::IpNextHopUnchanged,
+ Self::IpNextHop(ipv4_addr) => RouteMapSet::IpNextHop(*ipv4_addr),
+ Self::Ip6NextHopPeerAddress => RouteMapSet::Ip6NextHopPeerAddress,
+ Self::Ip6NextHopPreferGlobal => RouteMapSet::Ip6NextHopPreferGlobal,
+ Self::Ip6NextHop(ipv6_addr) => RouteMapSet::Ip6NextHop(*ipv6_addr),
+ Self::LocalPreference(local_preference) => {
+ RouteMapSet::LocalPreference(local_preference)
+ }
+ Self::Tag(tag) => RouteMapSet::Tag(tag),
+ Self::Weight(weight) => RouteMapSet::Weight(weight),
+ Self::Metric(metric) => RouteMapSet::Metric(metric),
+ Self::Src(src) => RouteMapSet::Src(src),
+ }
+ }
+ }
+
+ impl Into<FrrRouteMap> for RouteMapEntry {
+ fn into(self) -> FrrRouteMap {
+ FrrRouteMap {
+ seq: self.id.order,
+ action: match self.action {
+ RouteMapAction::Permit => proxmox_frr::ser::route_map::AccessAction::Permit,
+ RouteMapAction::Deny => proxmox_frr::ser::route_map::AccessAction::Deny,
+ },
+ matches: self
+ .match_actions
+ .into_iter()
+ .map(|match_action| match_action.into_inner().into())
+ .collect(),
+ sets: self
+ .set_actions
+ .into_iter()
+ .map(|set_action| set_action.into_inner().into())
+ .collect(),
+ custom_frr_config: Default::default(),
+ }
+ }
+ }
+
+ /// Add a list of Route Map Entries to a [`FrrConfig`].
+ ///
+ /// This method takes a list of Route Map Entries and adds them to given FRR configuration.
+ /// Existing Route Map entries with the same name, but different ordering number will remain in
+ /// the configuration. Entries with the same ordering will get merged.
+ ///
+ /// This behavior is different from Prefix Lists, where we overwrite existing Prefix Lists in
+ /// the FRR configuration. The reason for this is that users can override the Route Map setting
+ /// in the EVPN controller.
+ pub fn build_frr_route_maps(
+ config: impl IntoIterator<Item = RouteMap>,
+ frr_config: &mut FrrConfig,
+ ) -> Result<(), anyhow::Error> {
+ for route_map in config.into_iter() {
+ let RouteMap::RouteMapEntry(route_map) = route_map;
+ let route_map_name = RouteMapName::new(route_map.id.route_map_id.to_string());
+
+ if let Some(frr_route_map) = frr_config.routemaps.get_mut(&route_map_name) {
+ let idx =
+ frr_route_map.partition_point(|element| element.seq <= route_map.id().order());
+ frr_route_map.insert(idx, route_map.into());
+ } else {
+ frr_config
+ .routemaps
+ .insert(route_map_name, vec![route_map.into()]);
+ }
+ }
+
+ Ok(())
+ }
+
+ #[cfg(test)]
+ mod tests {
+ use super::*;
+
+ use proxmox_frr::ser::serializer::dump;
+ use proxmox_section_config::typed::ApiSectionDataEntry;
+
+ #[test]
+ fn test_build_route_map_order() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+route-map-entry: another_20
+ action deny
+
+route-map-entry: another_50
+ action deny
+
+route-map-entry: another_60
+ action deny
+
+route-map-entry: another_40
+ action deny
+
+route-map-entry: another_30
+ action deny
+"#;
+
+ let config = RouteMap::parse_section_config("route-maps.cfg", section_config)?;
+ let mut frr_config = FrrConfig::default();
+
+ build_frr_route_maps(
+ config
+ .into_iter()
+ .map(|(_, route_map_entry)| route_map_entry),
+ &mut frr_config,
+ )?;
+
+ assert_eq!(
+ dump(&frr_config)?,
+ r#"!
+route-map another deny 20
+exit
+!
+route-map another deny 30
+exit
+!
+route-map another deny 40
+exit
+!
+route-map another deny 50
+exit
+!
+route-map another deny 60
+exit
+"#
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_build_route_map() -> Result<(), anyhow::Error> {
+ let section_config = r#"
+route-map-entry: another_67
+ action deny
+ match key=vni,value=313373
+ match key=peer,value=some_peergroup
+
+route-map-entry: example_122
+ action deny
+ match key=route-type,value=es
+ match key=vni,value=313373
+ match key=ip-address-prefix-list,value=some_prefix_list
+ match key=ip-next-hop-prefix-list,value=some_other_prefix_list
+ match key=ip-next-hop-address,value=192.0.2.45
+ match key=metric,value=8347
+ match key=local-preference,value=8347
+ match key=peer,value=some_interface
+ match key=peer,value=some_peergroup
+ set key=ip6-next-hop-peer-address
+ set key=ip6-next-hop-prefer-global
+ set key=ip6-next-hop,value=2001:DB8::1
+
+route-map-entry: example_123
+ action permit
+ match key=ip6-address-prefix-list,value=some_prefix_list
+ match key=ip6-next-hop-prefix-list,value=some_other_prefix_list
+ match key=ip6-next-hop-address,value=2001:DB8:cafe::BeeF
+ set key=ip-next-hop-peer-address
+ set key=ip-next-hop-unchanged
+ set key=ip-next-hop,value=198.51.100.3
+ set key=local-preference,value=1234
+ set key=tag,value=untagged
+ set key=weight,value=20
+ set key=metric,value=+rtt
+"#;
+
+ let config = RouteMap::parse_section_config("route-maps.cfg", section_config)?;
+ let mut frr_config = FrrConfig::default();
+
+ build_frr_route_maps(
+ config
+ .into_iter()
+ .map(|(_, route_map_entry)| route_map_entry),
+ &mut frr_config,
+ )?;
+
+ assert_eq!(
+ dump(&frr_config)?,
+ r#"!
+route-map another deny 67
+ match evpn vni 313373
+ match peer some_peergroup
+exit
+!
+route-map example deny 122
+ match evpn route-type es
+ match evpn vni 313373
+ match ip address prefix-list some_prefix_list
+ match ip next-hop prefix-list some_other_prefix_list
+ match ip next-hop address 192.0.2.45
+ match metric 8347
+ match local-preference 8347
+ match peer some_interface
+ match peer some_peergroup
+ set ipv6 next-hop peer-address
+ set ipv6 next-hop prefer-global
+ set ipv6 next-hop global 2001:db8::1
+exit
+!
+route-map example permit 123
+ match ipv6 address prefix-list some_prefix_list
+ match ipv6 next-hop prefix-list some_other_prefix_list
+ match ipv6 next-hop address 2001:db8:cafe::beef
+ set ip next-hop peer-address
+ set ip next-hop unchanged
+ set ip next-hop 198.51.100.3
+ set local-preference 1234
+ set tag untagged
+ set weight 20
+ set metric +rtt
+exit
+"#
+ );
+
+ Ok(())
+ }
+ }
+}
+
pub mod api {
//! API type for Route Map Entries.
//!
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-ve-rs 9/9] ve-config: fabrics: adapt frr config generation to new format
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (9 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 8/9] ve-config: frr: implement frr config generation for route maps Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-perl-rs 1/3] pve-rs: sdn: add route maps module Stefan Hanreich
` (17 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
The format for representing match statements in route maps has
changed, adapt the FRR config generation for fabrics to use the new
format.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/fabric/frr.rs | 25 +++++++++----------------
1 file changed, 9 insertions(+), 16 deletions(-)
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index f2b7c72..dbb5924 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -5,8 +5,7 @@ use tracing;
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,
+ AccessAction, AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet,
};
use proxmox_frr::ser::{
self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap,
@@ -402,16 +401,12 @@ fn build_openfabric_routemap(
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"
- ))),
- )),
+ IpAddr::V4(_) => RouteMapMatch::IpAddressAccessList(AccessListName::new(format!(
+ "pve_openfabric_{fabric_id}_ips"
+ ))),
+ IpAddr::V6(_) => RouteMapMatch::Ip6AddressAccessList(AccessListName::new(format!(
+ "pve_openfabric_{fabric_id}_ip6s"
+ ))),
}],
sets: vec![RouteMapSet::Src(router_ip)],
custom_frr_config: Vec::new(),
@@ -430,10 +425,8 @@ fn build_ospf_dummy_routemap(
let routemap = RouteMapEntry {
seq,
action: AccessAction::Permit,
- matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address(
- AccessListOrPrefixList::AccessList(AccessListName::new(format!(
- "pve_ospf_{fabric_id}_ips"
- ))),
+ matches: vec![RouteMapMatch::IpAddressAccessList(AccessListName::new(
+ format!("pve_ospf_{fabric_id}_ips"),
))],
sets: vec![RouteMapSet::Src(IpAddr::from(router_ip))],
custom_frr_config: Vec::new(),
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-perl-rs 1/3] pve-rs: sdn: add route maps module
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (10 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 9/9] ve-config: fabrics: adapt frr config generation to new format Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-26 10:32 ` Wolfgang Bumiller
2026-03-25 9:41 ` [PATCH proxmox-perl-rs 2/3] pve-rs: sdn: add prefix lists module Stefan Hanreich
` (16 subsequent siblings)
28 siblings, 1 reply; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Exposes the functionality from ve-config to Perl by providing helpers
for instantiating the Rust configuration from Perl. The module also
contains the implementation for the CRUD API methods, which will be
used in the API methods in pve-network.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/Cargo.toml | 1 +
pve-rs/Makefile | 1 +
pve-rs/src/bindings/sdn/mod.rs | 3 +-
pve-rs/src/bindings/sdn/route_maps.rs | 243 ++++++++++++++++++++++++++
4 files changed, 247 insertions(+), 1 deletion(-)
create mode 100644 pve-rs/src/bindings/sdn/route_maps.rs
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index 45389b5..42d19fe 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -42,6 +42,7 @@ proxmox-notify = { version = "1", features = ["pve-context"] }
proxmox-oci = "0.2.1"
proxmox-openid = "1.0.2"
proxmox-resource-scheduling = "1.0.1"
+proxmox-schema = "5"
proxmox-section-config = "3"
proxmox-shared-cache = "1"
proxmox-subscription = "1"
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index 3bbc464..d662b00 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::RouteMaps \
PVE::RS::SDN \
PVE::RS::TFA
diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs
index fde3138..c571d28 100644
--- a/pve-rs/src/bindings/sdn/mod.rs
+++ b/pve-rs/src/bindings/sdn/mod.rs
@@ -1,4 +1,5 @@
pub(crate) mod fabrics;
+pub(crate) mod route_maps;
#[perlmod::package(name = "PVE::RS::SDN", lib = "pve_rs")]
pub mod pve_rs_sdn {
@@ -7,7 +8,7 @@ pub mod pve_rs_sdn {
//! This provides general methods for generating the frr config.
use anyhow::Error;
- use proxmox_frr::ser::{FrrConfig, serializer::to_raw_config};
+ use proxmox_frr::ser::{serializer::to_raw_config, FrrConfig};
use proxmox_ve_config::common::valid::Validatable;
use proxmox_ve_config::sdn::fabric::section_config::node::NodeId;
diff --git a/pve-rs/src/bindings/sdn/route_maps.rs b/pve-rs/src/bindings/sdn/route_maps.rs
new file mode 100644
index 0000000..b80126a
--- /dev/null
+++ b/pve-rs/src/bindings/sdn/route_maps.rs
@@ -0,0 +1,243 @@
+#[perlmod::package(name = "PVE::RS::SDN::RouteMaps", lib = "pve_rs")]
+pub mod pve_rs_sdn_route_maps {
+ //! The `PVE::RS::SDN::RouteMaps` package.
+
+ use std::collections::HashMap;
+ use std::ops::Deref;
+ use std::sync::Mutex;
+
+ use anyhow::{anyhow, Error};
+ use openssl::hash::{hash, MessageDigest};
+ use serde::{Deserialize, Serialize};
+
+ use perlmod::Value;
+
+ use proxmox_schema::Updater;
+ use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+ use proxmox_ve_config::sdn::route_map::api::RouteMapDeletableProperties;
+ use proxmox_ve_config::sdn::route_map::api::RouteMapEntry as ApiRouteMap;
+ use proxmox_ve_config::sdn::route_map::api::RouteMapEntryUpdater;
+ use proxmox_ve_config::sdn::route_map::RouteMap as ConfigRouteMap;
+ use proxmox_ve_config::sdn::route_map::RouteMapEntryId;
+ use proxmox_ve_config::sdn::route_map::RouteMapId;
+
+ /// A SDN RouteMap config instance.
+ #[derive(Serialize, Deserialize)]
+ pub struct PerlRouteMapConfig {
+ /// The route map config instance
+ pub route_maps: Mutex<HashMap<String, ConfigRouteMap>>,
+ }
+
+ perlmod::declare_magic!(Box<PerlRouteMapConfig> : &PerlRouteMapConfig as "PVE::RS::SDN::RouteMaps::Config");
+
+ /// Class method: Parse the raw configuration from `/etc/pve/sdn/route-maps.cfg`.
+ #[export]
+ pub fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, Error> {
+ let raw_config = std::str::from_utf8(raw_config)?;
+ let config = ConfigRouteMap::parse_section_config("route-maps.cfg", raw_config)?;
+
+ Ok(
+ perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlRouteMapConfig {
+ route_maps: Mutex::new(config.deref().clone()),
+ })),
+ )
+ }
+
+ /// Class method: Parse the configuration from `/etc/pve/sdn/.running_config`.
+ #[export]
+ pub fn running_config(
+ #[raw] class: Value,
+ route_maps: HashMap<String, ConfigRouteMap>,
+ ) -> Result<perlmod::Value, Error> {
+ Ok(
+ perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlRouteMapConfig {
+ route_maps: Mutex::new(route_maps.clone()),
+ })),
+ )
+ }
+
+ /// Used for writing the running configuration.
+ #[export]
+ pub fn to_sections(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ ) -> Result<HashMap<String, ConfigRouteMap>, Error> {
+ let config = this.route_maps.lock().unwrap();
+ Ok(config.deref().clone())
+ }
+
+ /// Method: Convert the configuration into the section config string.
+ ///
+ /// Used for writing `/etc/pve/sdn/route-maps.cfg`
+ #[export]
+ pub fn to_raw(#[try_from_ref] this: &PerlRouteMapConfig) -> Result<String, Error> {
+ let config = this.route_maps.lock().unwrap();
+ let route_maps: SectionConfigData<ConfigRouteMap> =
+ SectionConfigData::from_iter(config.deref().clone());
+
+ ConfigRouteMap::write_section_config("route-maps.cfg", &route_maps)
+ }
+
+ /// Method: Generate a digest for the whole configuration.
+ #[export]
+ pub fn digest(#[try_from_ref] this: &PerlRouteMapConfig) -> Result<String, Error> {
+ let config = to_raw(this)?;
+ let hash = hash(MessageDigest::sha256(), config.as_bytes())?;
+
+ Ok(hex::encode(hash))
+ }
+
+ /// Returns a list of all RouteMap entries.
+ #[export]
+ pub fn list(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ ) -> Result<HashMap<String, ApiRouteMap>, Error> {
+ Ok(this
+ .route_maps
+ .lock()
+ .unwrap()
+ .iter()
+ .map(|(id, route_map_entry)| {
+ let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
+ (id.clone(), route_map.clone().into())
+ })
+ .collect())
+ }
+
+ /// Returns a list of all RouteMap entries for a given RouteMap ID.
+ #[export]
+ pub fn list_route_map(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ route_map_id: RouteMapId,
+ ) -> Result<HashMap<String, ApiRouteMap>, Error> {
+ Ok(this
+ .route_maps
+ .lock()
+ .unwrap()
+ .iter()
+ .filter_map(|(id, route_map_entry)| {
+ let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
+
+ if route_map.id().route_map_id() == &route_map_id {
+ return Some((id.clone(), route_map.clone().into()));
+ }
+
+ None
+ })
+ .collect())
+ }
+
+ /// Create a new RouteMap entry.
+ #[export]
+ pub fn create(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ route_map: ApiRouteMap,
+ ) -> Result<(), Error> {
+ let mut route_maps = this.route_maps.lock().unwrap();
+
+ let id =
+ RouteMapEntryId::new(route_map.route_map_id().clone(), route_map.order()).to_string();
+ let config_route_map = ConfigRouteMap::RouteMapEntry(route_map.into());
+
+ if route_maps.get(&id).is_some() {
+ anyhow::bail!("route map entry already exists in configuration: {}", id);
+ }
+
+ route_maps.insert(id, config_route_map);
+
+ Ok(())
+ }
+
+ /// Returns a specfic entry of a RouteMap.
+ #[export]
+ pub fn get(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ route_map_id: RouteMapId,
+ order: u32,
+ ) -> Result<Option<ApiRouteMap>, Error> {
+ let id = RouteMapEntryId::new(route_map_id, order);
+
+ Ok(this
+ .route_maps
+ .lock()
+ .unwrap()
+ .iter()
+ .find(|(_id, route_map_entry)| {
+ let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
+ route_map.id() == &id
+ })
+ .map(|(_id, route_map_entry)| {
+ let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
+ route_map.clone().into()
+ }))
+ }
+
+ /// Update a RouteMap entry.
+ #[export]
+ pub fn update(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ route_map_id: RouteMapId,
+ order: u32,
+ updater: RouteMapEntryUpdater,
+ delete: Option<Vec<RouteMapDeletableProperties>>,
+ ) -> Result<(), Error> {
+ if updater.is_empty() && delete.is_empty() {
+ return Ok(());
+ }
+
+ let mut route_maps = this.route_maps.lock().unwrap();
+ let id = RouteMapEntryId::new(route_map_id, order).to_string();
+
+ let ConfigRouteMap::RouteMapEntry(route_map) = route_maps
+ .get_mut(&id)
+ .ok_or_else(|| anyhow!("Could not find route map with id: {}", id))?;
+
+ let RouteMapEntryUpdater {
+ action,
+ set_actions,
+ match_actions,
+ } = updater;
+
+ if let Some(action) = action {
+ route_map.set_action(action);
+ }
+
+ if let Some(match_actions) = match_actions {
+ route_map.set_match_actions(match_actions);
+ }
+
+ if let Some(set_actions) = set_actions {
+ route_map.set_set_actions(set_actions);
+ }
+
+ for deletable_property in delete.unwrap_or_default() {
+ match deletable_property {
+ RouteMapDeletableProperties::SetActions => {
+ route_map.set_set_actions(Vec::new());
+ }
+ RouteMapDeletableProperties::MatchActions => {
+ route_map.set_match_actions(Vec::new());
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Delete an entry in a RouteMap.
+ #[export]
+ pub fn delete(
+ #[try_from_ref] this: &PerlRouteMapConfig,
+ route_map_id: RouteMapId,
+ order: u32,
+ ) -> Result<(), Error> {
+ let id = RouteMapEntryId::new(route_map_id, order).to_string();
+
+ this.route_maps
+ .lock()
+ .unwrap()
+ .remove(&id.to_string())
+ .ok_or_else(|| anyhow!("could not find route map entry with id: {id}"))?;
+
+ Ok(())
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-perl-rs 2/3] pve-rs: sdn: add prefix lists module
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (11 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-perl-rs 1/3] pve-rs: sdn: add route maps module Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-perl-rs 3/3] sdn: add prefix list / route maps to frr config generation helper Stefan Hanreich
` (15 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Exposes the functionality from ve-config to Perl by providing helpers
for instantiating the Rust configuration from Perl. The module also
contains the implementation for the CRUD API methods, which will be
used for implementing the API methods in pve-network.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/Makefile | 1 +
pve-rs/src/bindings/sdn/mod.rs | 1 +
pve-rs/src/bindings/sdn/prefix_lists.rs | 199 ++++++++++++++++++++++++
3 files changed, 201 insertions(+)
create mode 100644 pve-rs/src/bindings/sdn/prefix_lists.rs
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index d662b00..e7dfe2e 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::PrefixLists \
PVE::RS::SDN::RouteMaps \
PVE::RS::SDN \
PVE::RS::TFA
diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs
index c571d28..9bddf1c 100644
--- a/pve-rs/src/bindings/sdn/mod.rs
+++ b/pve-rs/src/bindings/sdn/mod.rs
@@ -1,4 +1,5 @@
pub(crate) mod fabrics;
+pub(crate) mod prefix_lists;
pub(crate) mod route_maps;
#[perlmod::package(name = "PVE::RS::SDN", lib = "pve_rs")]
diff --git a/pve-rs/src/bindings/sdn/prefix_lists.rs b/pve-rs/src/bindings/sdn/prefix_lists.rs
new file mode 100644
index 0000000..52d30af
--- /dev/null
+++ b/pve-rs/src/bindings/sdn/prefix_lists.rs
@@ -0,0 +1,199 @@
+#[perlmod::package(name = "PVE::RS::SDN::PrefixLists", lib = "pve_rs")]
+pub mod pve_rs_sdn_prefix_lists {
+ //! The `PVE::RS::SDN::PrefixLists` package.
+ //!
+ //! This provides the configuration for the SDN fabrics, as well as helper methods for reading
+ //! / writing the configuration, as well as for generating ifupdown2 and FRR configuration.
+
+ use core::clone::Clone;
+ use std::collections::HashMap;
+ use std::ops::Deref;
+ use std::sync::Mutex;
+
+ use anyhow::{anyhow, Error};
+ use openssl::hash::{hash, MessageDigest};
+ use serde::{Deserialize, Serialize};
+
+ use perlmod::Value;
+ use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+ use proxmox_ve_config::sdn::prefix_list::api::{
+ PrefixList as ApiPrefixList, PrefixListDeletableProperties, PrefixListUpdater,
+ };
+ use proxmox_ve_config::sdn::prefix_list::{PrefixList as ConfigPrefixList, PrefixListId};
+
+ /// A SDN PrefixList config instance.
+ #[derive(Serialize, Deserialize)]
+ pub struct PerlPrefixListConfig {
+ /// The fabric config instance
+ pub prefix_lists: Mutex<HashMap<String, ConfigPrefixList>>,
+ }
+
+ perlmod::declare_magic!(Box<PerlPrefixListConfig> : &PerlPrefixListConfig as "PVE::RS::SDN::PrefixLists::Config");
+
+ /// Class method: Parse the raw configuration from `/etc/pve/sdn/prefix-lists.cfg`.
+ #[export]
+ pub fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, Error> {
+ let raw_config = std::str::from_utf8(raw_config)?;
+ let config = ConfigPrefixList::parse_section_config("prefix-lists.cfg", raw_config)?;
+
+ Ok(
+ perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlPrefixListConfig {
+ prefix_lists: Mutex::new(config.deref().clone()),
+ })),
+ )
+ }
+
+ /// Class method: Parse the configuration from `/etc/pve/sdn/.running_config`.
+ #[export]
+ pub fn running_config(
+ #[raw] class: Value,
+ prefix_lists: HashMap<String, ConfigPrefixList>,
+ ) -> Result<perlmod::Value, Error> {
+ let prefix_lists: SectionConfigData<ConfigPrefixList> =
+ SectionConfigData::from_iter(prefix_lists);
+
+ Ok(
+ perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlPrefixListConfig {
+ prefix_lists: Mutex::new(prefix_lists.deref().clone()),
+ })),
+ )
+ }
+
+ /// Used for writing the running configuration.
+ #[export]
+ pub fn to_sections(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ ) -> Result<HashMap<String, ConfigPrefixList>, Error> {
+ let config = this.prefix_lists.lock().unwrap();
+ Ok(config.deref().clone())
+ }
+
+ /// Method: Convert the configuration into the section config string.
+ ///
+ /// Used for writing `/etc/pve/sdn/prefix-lists.cfg`
+ #[export]
+ pub fn to_raw(#[try_from_ref] this: &PerlPrefixListConfig) -> Result<String, Error> {
+ let config = this.prefix_lists.lock().unwrap();
+
+ let prefix_lists: SectionConfigData<ConfigPrefixList> =
+ SectionConfigData::from_iter(config.deref().clone());
+
+ ConfigPrefixList::write_section_config("prefix-lists.cfg", &prefix_lists)
+ }
+
+ /// Method: Generate a digest for the whole configuration
+ #[export]
+ pub fn digest(#[try_from_ref] this: &PerlPrefixListConfig) -> Result<String, Error> {
+ let config = to_raw(this)?;
+ let hash = hash(MessageDigest::sha256(), config.as_bytes())?;
+
+ Ok(hex::encode(hash))
+ }
+
+ /// Returns a list of all PrefixLists
+ #[export]
+ pub fn list(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ ) -> Result<HashMap<String, ApiPrefixList>, Error> {
+ Ok(this
+ .prefix_lists
+ .lock()
+ .unwrap()
+ .iter()
+ .map(|(id, prefix_list)| {
+ let ConfigPrefixList::PrefixList(prefix_list) = prefix_list;
+ (id.clone(), prefix_list.clone())
+ })
+ .collect())
+ }
+
+ /// Create a new PrefixList.
+ #[export]
+ pub fn create(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ prefix_list: ApiPrefixList,
+ ) -> Result<(), Error> {
+ let mut prefix_lists = this.prefix_lists.lock().unwrap();
+
+ if prefix_lists.get(prefix_list.id().as_str()).is_some() {
+ anyhow::bail!(
+ "prefix list already exists in configuration: {}",
+ prefix_list.id()
+ );
+ }
+
+ prefix_lists.insert(
+ prefix_list.id().as_str().to_string(),
+ ConfigPrefixList::PrefixList(prefix_list),
+ );
+
+ Ok(())
+ }
+
+ /// Get a specific PrefixList.
+ #[export]
+ pub fn get(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ id: PrefixListId,
+ ) -> Result<Option<ApiPrefixList>, Error> {
+ Ok(this
+ .prefix_lists
+ .lock()
+ .unwrap()
+ .iter()
+ .find(|(_id, prefix_list)| {
+ let ConfigPrefixList::PrefixList(prefix_list) = prefix_list;
+ prefix_list.id() == &id
+ })
+ .map(|(_id, prefix_list)| {
+ let ConfigPrefixList::PrefixList(prefix_list) = prefix_list;
+ prefix_list.clone()
+ }))
+ }
+
+ /// Update a PrefixList.
+ #[export]
+ pub fn update(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ id: PrefixListId,
+ updater: PrefixListUpdater,
+ delete: Option<Vec<PrefixListDeletableProperties>>,
+ ) -> Result<(), Error> {
+ let mut prefix_lists = this.prefix_lists.lock().unwrap();
+
+ let ConfigPrefixList::PrefixList(prefix_list) = prefix_lists
+ .get_mut(id.as_str())
+ .ok_or_else(|| anyhow!("Could not find prefix list with id: {}", id))?;
+
+ let PrefixListUpdater { entries } = updater;
+
+ if let Some(entries) = entries {
+ prefix_list.entries = entries;
+ }
+
+ for deletable_property in delete.unwrap_or_default() {
+ match deletable_property {
+ PrefixListDeletableProperties::Entries => {
+ prefix_list.entries = Vec::new();
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Delete a PrefixList.
+ #[export]
+ pub fn delete(
+ #[try_from_ref] this: &PerlPrefixListConfig,
+ id: PrefixListId,
+ ) -> Result<(), Error> {
+ this.prefix_lists
+ .lock()
+ .unwrap()
+ .remove(&id.to_string())
+ .ok_or_else(|| anyhow!("could not find route map entry with id: {id}"))?;
+
+ Ok(())
+ }
+}
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH proxmox-perl-rs 3/3] sdn: add prefix list / route maps to frr config generation helper
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (12 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-perl-rs 2/3] pve-rs: sdn: add prefix lists module Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 01/13] controller: bgp: evpn: adapt to new match / set frr config syntax Stefan Hanreich
` (14 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
With the addition of prefix lists and route maps, the helper needs to
be adapted so the FRR configuration for prefix lists and route maps
gets generated as well. Prefix lists and route maps are generated
before generating the fabric configuration, so it is possible for the
fabric config generation logic to check for the existence of prefix
lists and route maps. It also ensures that
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/mod.rs | 26 ++++++++++++++++++++++++--
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs
index 9bddf1c..c6361c3 100644
--- a/pve-rs/src/bindings/sdn/mod.rs
+++ b/pve-rs/src/bindings/sdn/mod.rs
@@ -15,17 +15,39 @@ pub mod pve_rs_sdn {
use proxmox_ve_config::sdn::fabric::section_config::node::NodeId;
use crate::bindings::pve_rs_sdn_fabrics::PerlFabricConfig;
+ use crate::bindings::sdn::prefix_lists::pve_rs_sdn_prefix_lists::PerlPrefixListConfig;
+ use crate::bindings::sdn::route_maps::pve_rs_sdn_route_maps::PerlRouteMapConfig;
/// 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,
+ #[try_from_ref] prefix_list_config: &PerlPrefixListConfig,
+ #[try_from_ref] route_map_config: &PerlRouteMapConfig,
+ #[try_from_ref] fabric_config: &PerlFabricConfig,
node_id: NodeId,
) -> Result<Vec<String>, Error> {
- let fabric_config = cfg.fabric_config.lock().unwrap().clone().into_valid()?;
+ let prefix_list_config = prefix_list_config.prefix_lists.lock().unwrap();
+ proxmox_ve_config::sdn::prefix_list::frr::build_frr_prefix_lists(
+ prefix_list_config.values().cloned(),
+ &mut frr_config,
+ )?;
+
+ let route_map_config = route_map_config.route_maps.lock().unwrap();
+ proxmox_ve_config::sdn::route_map::frr::build_frr_route_maps(
+ route_map_config.values().cloned(),
+ &mut frr_config,
+ )?;
+
+ let fabric_config = fabric_config
+ .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] 62+ messages in thread
* [PATCH pve-network 01/13] controller: bgp: evpn: adapt to new match / set frr config syntax
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (13 preceding siblings ...)
2026-03-25 9:41 ` [PATCH proxmox-perl-rs 3/3] sdn: add prefix list / route maps to frr config generation helper Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-26 15:19 ` Hannes Laimer
2026-03-25 9:41 ` [PATCH pve-network 02/13] sdn: add prefix lists module Stefan Hanreich
` (13 subsequent siblings)
28 siblings, 1 reply; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
proxmox-frr has changed the representation of match / set statements
in its structs. It has changed to generic key / value fields, where
the keys specify the literal name of the key in the FRR configuration.
Adapt the controllers to use the new fields so the configuration gets
generated properly.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 7 +++---
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 24 ++++++++-----------
2 files changed, 13 insertions(+), 18 deletions(-)
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
index 8891541..d54c9ec 100644
--- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
@@ -136,13 +136,12 @@ sub generate_frr_config {
$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' },
+ key => 'ip address prefix-list',
+ value => 'loopbacks_ips',
};
my $routemap = {
matches => [$routemap_config],
- sets => [{ set_type => 'src', value => $ifaceip }],
+ sets => [{ key => 'src', value => $ifaceip }],
action => "permit",
seq => 1,
};
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index d2825f5..3e643b1 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -329,9 +329,8 @@ sub generate_zone_frr_config {
if (!$exitnodes_primary || $exitnodes_primary eq $local_node) {
# 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' },
+ key => 'ipv6 address prefix-list',
+ value => 'only_default_v6',
};
my $routemap_v6 = { seq => 1, matches => [$routemap_config_v6], action => "deny" };
unshift(
@@ -339,23 +338,21 @@ sub generate_zone_frr_config {
);
my $routemap_config = {
- protocol_type => 'ip',
- match_type => 'address',
- value => { list_type => 'prefixlist', list_name => 'only_default' },
+ key => 'ip address prefix-list',
+ value => '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 = {
- protocol_type => 'ipv6',
- match_type => 'address',
- value => { list_type => 'prefixlist', list_name => 'only_default_v6' },
+ key => 'ipv6 address prefix-list',
+ value => 'only_default_v6',
};
my $routemap_v6 = {
seq => 1,
matches => [$routemap_config_v6],
- sets => [{ set_type => 'metric', value => 200 }],
+ sets => [{ key => 'metric', value => 200 }],
action => "permit",
};
unshift(
@@ -363,14 +360,13 @@ sub generate_zone_frr_config {
);
my $routemap_config = {
- protocol_type => 'ip',
- match_type => 'address',
- value => { list_type => 'prefixlist', list_name => 'only_default' },
+ key => 'ip address prefix-list',
+ value => 'only_default',
};
my $routemap = {
seq => 1,
matches => [$routemap_config],
- sets => [{ set_type => 'metric', value => 200 }],
+ sets => [{ key => 'metric', value => 200 }],
action => "permit",
};
unshift(@{ $config->{frr}->{routemaps}->{'MAP_VTEP_OUT'} }, $routemap);
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 02/13] sdn: add prefix lists module
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (14 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 01/13] controller: bgp: evpn: adapt to new match / set frr config syntax Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 03/13] api2: add prefix list module Stefan Hanreich
` (12 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Defines helpers for common operations (reading / writing
configuration) as well as the required formats / schema definitions
for the route map API.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Makefile | 14 ++-
src/PVE/Network/SDN/PrefixLists.pm | 134 +++++++++++++++++++++++++++++
2 files changed, 147 insertions(+), 1 deletion(-)
create mode 100644 src/PVE/Network/SDN/PrefixLists.pm
diff --git a/src/PVE/Network/SDN/Makefile b/src/PVE/Network/SDN/Makefile
index d1ffef9..fa6702e 100644
--- a/src/PVE/Network/SDN/Makefile
+++ b/src/PVE/Network/SDN/Makefile
@@ -1,4 +1,16 @@
-SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm Dns.pm Dhcp.pm Fabrics.pm Frr.pm
+SOURCES=Vnets.pm\
+ VnetPlugin.pm\
+ Zones.pm\
+ Controllers.pm\
+ Subnets.pm\
+ SubnetPlugin.pm\
+ Ipams.pm\
+ Dns.pm\
+ Dhcp.pm\
+ Fabrics.pm\
+ Frr.pm\
+ RouteMaps.pm\
+ PrefixLists.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/Network/SDN/PrefixLists.pm b/src/PVE/Network/SDN/PrefixLists.pm
new file mode 100644
index 0000000..2ddb858
--- /dev/null
+++ b/src/PVE/Network/SDN/PrefixLists.pm
@@ -0,0 +1,134 @@
+package PVE::Network::SDN::PrefixLists;
+
+use strict;
+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::Network::SDN;
+use PVE::RS::SDN::PrefixLists;
+
+PVE::JSONSchema::register_format(
+ 'pve-sdn-prefix-list-id',
+ sub {
+ my ($id, $noerr) = @_;
+
+ if ($id =~ m/^(only_default|only_default_v6|loopback_ips|)$/) {
+ return undef if $noerr;
+ die "prefix list ID '$id' is currently reserved and cannot be used\n";
+ }
+
+ if ($id !~ m/^[a-zA-Z0-9][a-zA-Z0-9-_]{0,30}[a-zA-Z0-9]?$/i) {
+ return undef if $noerr;
+ die "prefix list ID '$id' contains illegal characters\n";
+ }
+
+ return $id;
+ },
+);
+
+PVE::JSONSchema::register_standard_option(
+ 'pve-sdn-prefix-list-id',
+ {
+ description => "The SDN prefix list identifier",
+ type => 'string',
+ format => 'pve-sdn-prefix-list-id',
+ },
+);
+
+cfs_register_file(
+ 'sdn/prefix-lists.cfg', \&parse_prefix_lists_config, \&write_prefix_lists_config,
+);
+
+sub parse_prefix_lists_config {
+ my ($filename, $raw) = @_;
+ return $raw // '';
+}
+
+sub write_prefix_lists_config {
+ my ($filename, $config) = @_;
+ return $config // '';
+}
+
+sub config {
+ my ($running) = @_;
+
+ if ($running) {
+ my $running_config = PVE::Network::SDN::running_config();
+
+ # if the config hasn't yet been applied after the introduction of
+ # prefix lists then the key does not exist in the running config so we
+ # default to an empty hash
+ my $prefix_lists_config = $running_config->{'prefix-lists'}->{ids} // {};
+ return PVE::RS::SDN::PrefixLists->running_config($prefix_lists_config);
+ }
+
+ my $prefix_lists_config = cfs_read_file("sdn/prefix-lists.cfg");
+ return PVE::RS::SDN::PrefixLists->config($prefix_lists_config);
+}
+
+sub write_config {
+ my ($config) = @_;
+ cfs_write_file("sdn/prefix-lists.cfg", $config->to_raw(), 1);
+}
+
+sub prefix_list_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ digest => get_standard_option('pve-config-digest'),
+ entries => {
+ type => 'array',
+ optional => $update,
+ items => {
+ type => 'string',
+ format => {
+ action => {
+ type => 'string',
+ enum => ['permit', 'deny'],
+ },
+ prefix => {
+ type => 'string',
+ format => 'CIDR',
+ },
+ le => {
+ type => 'integer',
+ minimum => 0,
+ maximum => 128,
+ optional => 1,
+ },
+ ge => {
+ type => 'integer',
+ minimum => 0,
+ maximum => 128,
+ optional => 1,
+ },
+ seq => {
+ type => 'integer',
+ minimum => 0,
+ maximum => 2 ** 32 - 1,
+ optional => 1,
+ },
+ }
+ }
+ }
+ };
+
+ if ($update) {
+ $properties->{delete} = {
+ type => 'array',
+ optional => 1,
+ items => {
+ type => 'string',
+ enum => ['entries'],
+ }
+ };
+ } else {
+ $properties->{id} = get_standard_option('pve-sdn-prefix-list-id');
+ }
+
+ return $properties;
+}
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 03/13] api2: add prefix list module
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (15 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 02/13] sdn: add prefix lists module Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-26 15:01 ` Hannes Laimer
2026-03-25 9:41 ` [PATCH pve-network 04/13] sdn: add route map module Stefan Hanreich
` (11 subsequent siblings)
28 siblings, 1 reply; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Contains the CRUD functionality for prefix lists:
GET /prefix-lists - lists all prefix lists
GET /prefix-lists/<id> - get prefix list <id>
POST /prefix-lists - create a new prefix list
PUT /prefix-lists/<id> - update prefix list <id>
DELETE /prefix-lists/<id> - delete prefix list <id>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN.pm | 7 +
src/PVE/API2/Network/SDN/Makefile | 11 +-
src/PVE/API2/Network/SDN/PrefixLists.pm | 247 ++++++++++++++++++++++++
3 files changed, 263 insertions(+), 2 deletions(-)
create mode 100644 src/PVE/API2/Network/SDN/PrefixLists.pm
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index 34652a5..d999ad0 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -22,6 +22,7 @@ use PVE::API2::Network::SDN::Zones;
use PVE::API2::Network::SDN::Ipams;
use PVE::API2::Network::SDN::Dns;
use PVE::API2::Network::SDN::Fabrics;
+use PVE::API2::Network::SDN::PrefixLists;
use base qw(PVE::RESTHandler);
@@ -55,6 +56,11 @@ __PACKAGE__->register_method({
path => 'fabrics',
});
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::PrefixLists",
+ path => 'prefix-lists',
+});
+
__PACKAGE__->register_method({
name => 'index',
path => '',
@@ -87,6 +93,7 @@ __PACKAGE__->register_method({
{ id => 'ipams' },
{ id => 'dns' },
{ id => 'fabrics' },
+ { id => 'prefix-lists' },
];
return $res;
diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
index 2624d9a..4349c17 100644
--- a/src/PVE/API2/Network/SDN/Makefile
+++ b/src/PVE/API2/Network/SDN/Makefile
@@ -1,5 +1,12 @@
-SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm Dns.pm Ips.pm Fabrics.pm
-
+SOURCES=Vnets.pm\
+ Zones.pm\
+ Controllers.pm\
+ Subnets.pm\
+ Ipams.pm\
+ Dns.pm\
+ Ips.pm\
+ Fabrics.pm\
+ PrefixLists.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/API2/Network/SDN/PrefixLists.pm b/src/PVE/API2/Network/SDN/PrefixLists.pm
new file mode 100644
index 0000000..2ac6481
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/PrefixLists.pm
@@ -0,0 +1,247 @@
+package PVE::API2::Network::SDN::PrefixLists;
+
+use strict;
+use warnings;
+
+use PVE::Exception qw(raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'list_prefix_lists',
+ path => '',
+ method => 'GET',
+ permissions => {
+ description =>
+ "Only returns prefix list entries where you have 'Sys.Audit' or 'Sys.Modify' permissions.",
+ },
+ description => "List Prefix Lists",
+ parameters => {
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display running config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display pending config.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {},
+ },
+ links => [{ rel => 'child', href => "{id}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $pending = extract_param($param, 'pending');
+ my $running = extract_param($param, 'running');
+
+ my $digest;
+ my $prefix_lists;
+
+ if ($pending) {
+ my $current_config = PVE::Network::SDN::PrefixLists::config()->list();
+ my $running_config = PVE::Network::SDN::PrefixLists::config(1)->list();
+
+ my $pending_prefix_lists = PVE::Network::SDN::pending_config(
+ $running_config,
+ $current_config,
+ 'prefix-lists',
+ );
+
+ $digest = $current_config->digest();
+ $prefix_lists = $pending_prefix_lists->{ids}
+ } elsif ($running) {
+ $prefix_lists = PVE::Network::SDN::PrefixLists::config(1)->list();
+ } else {
+ my $current_config = PVE::Network::SDN::PrefixLists::config();
+
+ $digest = $current_config->digest();
+ $prefix_lists = $current_config->list();
+ }
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $prefix_list_privs = ['SDN.Audit'];
+
+ my @res;
+ for my $prefix_list_id (sort keys $prefix_lists->%*) {
+ next if !$rpcenv->check_any($authuser, "/prefix-lists/$prefix_list_id", $prefix_list_privs, 1);
+ $prefix_lists->{$prefix_list_id}->{digest} = $digest if $digest;
+ push @res, $prefix_lists->{$prefix_list_id};
+ }
+
+ return \@res;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'get_prefix_list_entry',
+ path => '{id}',
+ method => 'GET',
+ permissions => {
+ check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Audit']],
+ },
+ description => "Get Prefix List",
+ parameters => {
+ properties => {
+ id => get_standard_option('pve-sdn-prefix-list-id'),
+ },
+ },
+ returns => {
+ type => "object",
+ properties => {}
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $prefix_list_id = extract_param($param, 'id');
+ my $prefix_list_entry = PVE::Network::SDN::PrefixLists::config()->get($prefix_list_id);
+
+ raise_param_exc({ 'id' => "$prefix_list_id doesn't exist" })
+ if !$prefix_list_entry;
+
+ return $prefix_list_entry;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'create_prefix_list_entry',
+ path => '',
+ method => 'POST',
+ permissions => {
+ check => ['perm', '/sdn/prefix-lists', ['SDN.Allocate']],
+ },
+ description => "Create Prefix List",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ PVE::Network::SDN::PrefixLists::prefix_list_properties(0)->%*,
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::PrefixLists::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $config->create($param);
+ PVE::Network::SDN::PrefixLists::write_config($config);
+ },
+ "creating prefix list failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update_prefix_list_entry',
+ path => '{id}',
+ method => 'PUT',
+ permissions => {
+ check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Allocate']],
+ },
+ description => "Update Prefix List",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ PVE::Network::SDN::PrefixLists::prefix_list_properties(1)->%*,
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::PrefixLists::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ my $prefix_list_id = extract_param($param, 'id');
+ my $delete = extract_param($param, 'delete');
+
+ $config->update($prefix_list_id, $param, $delete);
+ PVE::Network::SDN::PrefixLists::write_config($config);
+ },
+ "updating prefix list failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'delete_prefix_list_entry',
+ path => '{id}',
+ method => 'DELETE',
+ permissions => {
+ check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Allocate']],
+ },
+ description => "Delete Prefix List",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ id => get_standard_option('pve-sdn-prefix-list-id'),
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::PrefixLists::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ my $prefix_list_id = extract_param($param, 'id');
+
+ $config->delete($prefix_list_id);
+ PVE::Network::SDN::PrefixLists::write_config($config);
+ },
+ "deleting prefix list failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 04/13] sdn: add route map module
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (16 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 03/13] api2: add prefix list module Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 05/13] api2: add route maps api module Stefan Hanreich
` (10 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Defines helpers for common operations (reading / writing
configuration) as well as the required formats / schema definitions
for the route map API.
The Route Map ID format currently rejects all IDs that could be
auto-generated by PVE entities, to prevent accidental overrides of
built-in route maps. Instead of re-defining route maps, users can
create a new custom route map and select that in the EVPN / BGP
controller, if they want to override the auto-generated route map.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Makefile | 4 +-
src/PVE/API2/Network/SDN/RouteMaps/Makefile | 8 +
src/PVE/Network/SDN/RouteMaps.pm | 173 ++++++++++++++++++++
3 files changed, 184 insertions(+), 1 deletion(-)
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/Makefile
create mode 100644 src/PVE/Network/SDN/RouteMaps.pm
diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
index 4349c17..770eef2 100644
--- a/src/PVE/API2/Network/SDN/Makefile
+++ b/src/PVE/API2/Network/SDN/Makefile
@@ -6,7 +6,8 @@ SOURCES=Vnets.pm\
Dns.pm\
Ips.pm\
Fabrics.pm\
- PrefixLists.pm
+ PrefixLists.pm\
+ RouteMaps.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
@@ -15,4 +16,5 @@ install:
for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/$$i; done
make -C Fabrics install
make -C Nodes install
+ make -C RouteMaps install
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/Makefile b/src/PVE/API2/Network/SDN/RouteMaps/Makefile
new file mode 100644
index 0000000..3d0a928
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/RouteMaps/Makefile
@@ -0,0 +1,8 @@
+SOURCES=RouteMap.pm\
+
+
+PERL5DIR=${DESTDIR}/usr/share/perl5
+
+.PHONY: install
+install:
+ for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/RouteMaps/$$i; done
diff --git a/src/PVE/Network/SDN/RouteMaps.pm b/src/PVE/Network/SDN/RouteMaps.pm
new file mode 100644
index 0000000..1d2f495
--- /dev/null
+++ b/src/PVE/Network/SDN/RouteMaps.pm
@@ -0,0 +1,173 @@
+package PVE::Network::SDN::RouteMaps;
+
+use strict;
+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::Network::SDN;
+use PVE::RS::SDN::RouteMaps;
+
+PVE::JSONSchema::register_format(
+ 'pve-sdn-route-map-id',
+ sub {
+ my ($id, $noerr) = @_;
+
+ if ($id =~ m/^(pve_.*|MAP_VTEP_IN|MAP_VTEP_OUT|correct_src)$/) {
+ return undef if $noerr;
+ die "route map ID '$id' is currently reserved and cannot be used\n";
+ }
+
+ if ($id !~ m/^[a-zA-Z0-9][a-zA-Z0-9-_]{0,30}[a-zA-Z0-9]?$/i) {
+ return undef if $noerr;
+ die "route map ID '$id' contains illegal characters\n";
+ }
+
+ return $id;
+ },
+);
+
+PVE::JSONSchema::register_standard_option(
+ 'pve-sdn-route-map-id',
+ {
+ description => "The SDN route map identifier",
+ type => 'string',
+ format => 'pve-sdn-route-map-id',
+ },
+);
+
+PVE::JSONSchema::register_standard_option(
+ 'pve-sdn-route-map-order',
+ {
+ description => 'The index of this route map entry',
+ type => 'integer',
+ minimum => 0,
+ maximum => 2 ** 32 - 1,
+ },
+);
+
+cfs_register_file(
+ 'sdn/route-maps.cfg', \&parse_route_maps_config, \&write_route_maps_config,
+);
+
+sub parse_route_maps_config {
+ my ($filename, $raw) = @_;
+ return $raw // '';
+}
+
+sub write_route_maps_config {
+ my ($filename, $config) = @_;
+ return $config // '';
+}
+
+sub config {
+ my ($running) = @_;
+
+ if ($running) {
+ my $running_config = PVE::Network::SDN::running_config();
+
+ # if the config hasn't yet been applied after the introduction of
+ # route maps then the key does not exist in the running config so we
+ # default to an empty hash
+ my $route_maps_config = $running_config->{'route-maps'}->{ids} // {};
+ return PVE::RS::SDN::RouteMaps->running_config($route_maps_config);
+ }
+
+ my $route_map_config = cfs_read_file("sdn/route-maps.cfg");
+ return PVE::RS::SDN::RouteMaps->config($route_map_config);
+}
+
+sub write_config {
+ my ($config) = @_;
+ cfs_write_file("sdn/route-maps.cfg", $config->to_raw(), 1);
+}
+
+sub route_map_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ 'route-map-id' => get_standard_option('pve-sdn-route-map-id'),
+ 'order' => get_standard_option('pve-sdn-route-map-order'),
+ digest => get_standard_option('pve-config-digest'),
+ action => {
+ description => 'Matching policy of a route map entry.',
+ type => 'string',
+ enum => ['permit', 'deny'],
+ optional => $update,
+ },
+ set => {
+ type => 'array',
+ items => {
+ type => 'string',
+ format => {
+ key => {
+ type => 'string',
+ enum => [
+ 'ip-next-hop-peer-address',
+ 'ip-next-hop',
+ 'ip-next-hop-unchanged',
+ 'ip6-next-hop-peer-address',
+ 'ip6-next-hop-prefer-global',
+ 'ip6-next-hop',
+ 'local-preference',
+ 'tag',
+ 'weight',
+ 'metric',
+ 'src',
+ ]
+ },
+ value => {
+ type => 'string',
+ optional => 1,
+ }
+ },
+ },
+ optional => 1,
+ },
+ match => {
+ type => 'array',
+ items => {
+ type => 'string',
+ format => {
+ key => {
+ type => 'string',
+ enum => [
+ 'route-type',
+ 'vni',
+ 'ip-address-prefix-list',
+ 'ip6-address-prefix-list',
+ 'ip-next-hop-prefix-list',
+ 'ip6-next-hop-prefix-list',
+ 'ip-next-hop-address',
+ 'ip6-next-hop-address',
+ 'metric',
+ 'local-preference',
+ 'peer',
+ ]
+ },
+ value => {
+ type => 'string',
+ optional => 1,
+ }
+ },
+ },
+ optional => 1,
+ },
+ };
+
+ if ($update) {
+ $properties->{delete} = {
+ type => 'array',
+ optional => 1,
+ items => {
+ type => 'string',
+ enum => ['set', 'match'],
+ }
+ };
+ }
+
+ return $properties;
+}
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 05/13] api2: add route maps api module
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (17 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 04/13] sdn: add route map module Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-26 15:05 ` Hannes Laimer
2026-03-25 9:41 ` [PATCH pve-network 06/13] api2: add route map module Stefan Hanreich
` (9 subsequent siblings)
28 siblings, 1 reply; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Contains the following API endpoints:
GET /route-maps - lists all route map entries
POST /route-maps - creates a new route map entry
The following commits contain the API modules for accessing specific
route maps / route map entries.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN.pm | 7 ++
src/PVE/API2/Network/SDN/RouteMaps.pm | 131 ++++++++++++++++++++++++++
2 files changed, 138 insertions(+)
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps.pm
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index d999ad0..16d1536 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -23,6 +23,7 @@ use PVE::API2::Network::SDN::Ipams;
use PVE::API2::Network::SDN::Dns;
use PVE::API2::Network::SDN::Fabrics;
use PVE::API2::Network::SDN::PrefixLists;
+use PVE::API2::Network::SDN::RouteMaps;
use base qw(PVE::RESTHandler);
@@ -61,6 +62,11 @@ __PACKAGE__->register_method({
path => 'prefix-lists',
});
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::RouteMaps",
+ path => 'route-maps',
+});
+
__PACKAGE__->register_method({
name => 'index',
path => '',
@@ -94,6 +100,7 @@ __PACKAGE__->register_method({
{ id => 'dns' },
{ id => 'fabrics' },
{ id => 'prefix-lists' },
+ { id => 'route-maps' },
];
return $res;
diff --git a/src/PVE/API2/Network/SDN/RouteMaps.pm b/src/PVE/API2/Network/SDN/RouteMaps.pm
new file mode 100644
index 0000000..f3c3583
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/RouteMaps.pm
@@ -0,0 +1,131 @@
+package PVE::API2::Network::SDN::RouteMaps;
+
+use strict;
+use warnings;
+
+use PVE::Exception qw(raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Network::SDN::RouteMaps;
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'list_route_maps',
+ path => '',
+ method => 'GET',
+ permissions => {
+ description =>
+ "Only returns route map entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions.",
+ },
+ description => "List Route Maps",
+ parameters => {
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display running config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display pending config.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => PVE::Network::SDN::RouteMaps::route_map_properties(0),
+ },
+ links => [{ rel => 'child', href => "{route-map-id}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $pending = extract_param($param, 'pending');
+ my $running = extract_param($param, 'running');
+
+ my $digest;
+ my $route_maps;
+
+ if ($pending) {
+ my $current_config = PVE::Network::SDN::RouteMaps::config()->list();
+ my $running_config = PVE::Network::SDN::RouteMaps::config(1)->list();
+
+ my $pending_route_maps = PVE::Network::SDN::pending_config(
+ $running_config,
+ $current_config,
+ 'route-maps',
+ );
+
+ $digest = $current_config->digest();
+ $route_maps = $pending_route_maps->{ids}
+ } elsif ($running) {
+ $route_maps = PVE::Network::SDN::RouteMaps::config(1)->list();
+ } else {
+ my $current_config = PVE::Network::SDN::RouteMaps::config();
+
+ $digest = $current_config->digest();
+ $route_maps = $current_config->list();
+ }
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $route_map_privs = ['SDN.Audit', 'SDN.Allocate'];
+
+ my @res;
+ for my $route_map_id (sort keys $route_maps->%*) {
+ next if !$rpcenv->check_any($authuser, "/route-maps/$route_map_id", $route_map_privs, 1);
+ $route_maps->{$route_map_id}->{digest} = $digest if $digest;
+ push @res, $route_maps->{$route_map_id};
+ }
+
+ return \@res;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'create_route_map_entry',
+ path => '',
+ method => 'POST',
+ permissions => {
+ check => ['perm', '/sdn/route-maps', ['SDN.Allocate']],
+ },
+ description => "Create Route Map entry",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ PVE::Network::SDN::RouteMaps::route_map_properties(0)->%*,
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::RouteMaps::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $config->create($param);
+ PVE::Network::SDN::RouteMaps::write_config($config);
+ },
+ "creating route map entry failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 06/13] api2: add route map module
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (18 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 05/13] api2: add route maps api module Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-26 15:07 ` Hannes Laimer
2026-03-25 9:41 ` [PATCH pve-network 07/13] api2: add route map entry module Stefan Hanreich
` (8 subsequent siblings)
28 siblings, 1 reply; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
This module contains the following API endpoint:
GET /route-maps/<id> - lists all route map entries for the route map
<id>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/RouteMaps.pm | 6 ++
.../API2/Network/SDN/RouteMaps/RouteMap.pm | 86 +++++++++++++++++++
2 files changed, 92 insertions(+)
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
diff --git a/src/PVE/API2/Network/SDN/RouteMaps.pm b/src/PVE/API2/Network/SDN/RouteMaps.pm
index f3c3583..a4ca649 100644
--- a/src/PVE/API2/Network/SDN/RouteMaps.pm
+++ b/src/PVE/API2/Network/SDN/RouteMaps.pm
@@ -3,6 +3,7 @@ package PVE::API2::Network::SDN::RouteMaps;
use strict;
use warnings;
+use PVE::API2::Network::SDN::RouteMaps::RouteMap;
use PVE::Exception qw(raise_param_exc);
use PVE::JSONSchema qw(get_standard_option);
use PVE::Network::SDN::RouteMaps;
@@ -11,6 +12,11 @@ use PVE::Tools qw(extract_param);
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::RouteMaps::RouteMap",
+ path => '{route-map-id}',
+});
+
__PACKAGE__->register_method({
name => 'list_route_maps',
path => '',
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm b/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
new file mode 100644
index 0000000..93a3165
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
@@ -0,0 +1,86 @@
+package PVE::API2::Network::SDN::RouteMaps::RouteMap;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Exception qw(raise_param_exc);
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'list_route_map_entries',
+ path => '',
+ method => 'GET',
+ permissions => {
+ check => ['perm', '/sdn/route-maps/{route_map_id}', ['SDN.Audit']],
+ },
+ description => "List all entries for a given Route Map",
+ parameters => {
+ properties => {
+ 'route-map-id' => get_standard_option('pve-sdn-route-map-id'),
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display running config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display pending config.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => PVE::Network::SDN::RouteMaps::route_map_properties(0),
+ },
+ links => [{ rel => 'child', href => "{order}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $pending = extract_param($param, 'pending');
+ my $running = extract_param($param, 'running');
+ my $route_map_id = extract_param($param, 'route-map-id');
+
+ my $digest;
+ my $route_map_entries;
+
+ if ($pending) {
+ my $current_config = PVE::Network::SDN::RouteMaps::config()->list_route_map($route_map_id);
+ my $running_config = PVE::Network::SDN::RouteMaps::config(1)->list_route_map($route_map_id);
+
+ my $pending_route_maps = PVE::Network::SDN::pending_config(
+ $running_config,
+ $current_config,
+ 'route-maps',
+ );
+
+ $digest = $current_config->digest();
+ $route_map_entries = $pending_route_maps->{ids}
+ } elsif ($running) {
+ $route_map_entries = PVE::Network::SDN::RouteMaps::config(1)->list_route_map($route_map_id);
+ } else {
+ my $current_config = PVE::Network::SDN::RouteMaps::config();
+
+ $digest = $current_config->digest();
+ $route_map_entries = $current_config->list_route_map($route_map_id);
+ }
+
+ my @res;
+ for my $route_map_id (sort keys $route_map_entries->%*) {
+ $route_map_entries->{$route_map_id}->{digest} = $digest if $digest;
+ push @res, $route_map_entries->{$route_map_id};
+ }
+
+ return \@res;
+ },
+});
+
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 07/13] api2: add route map entry module
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (19 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 06/13] api2: add route map module Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-26 15:13 ` Hannes Laimer
2026-03-25 9:41 ` [PATCH pve-network 08/13] evpn controller: add route_map_{in,out} parameter Stefan Hanreich
` (7 subsequent siblings)
28 siblings, 1 reply; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
This module contains the following API endpoints:
GET /route-maps/<id>/<order> - gets the order'th entry in route map
<id>
PUT /route-maps/<id>/<order> - updates the order'th entry in
route map <id>
DELETE /route-maps/<id>/<order> - deletes the order'th
entry in route map <id>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/RouteMaps/Makefile | 1 +
.../API2/Network/SDN/RouteMaps/RouteMap.pm | 6 +
.../Network/SDN/RouteMaps/RouteMapEntry.pm | 136 ++++++++++++++++++
3 files changed, 143 insertions(+)
create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/Makefile b/src/PVE/API2/Network/SDN/RouteMaps/Makefile
index 3d0a928..07b45e9 100644
--- a/src/PVE/API2/Network/SDN/RouteMaps/Makefile
+++ b/src/PVE/API2/Network/SDN/RouteMaps/Makefile
@@ -1,4 +1,5 @@
SOURCES=RouteMap.pm\
+ RouteMapEntry.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm b/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
index 93a3165..1eeb9d6 100644
--- a/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
+++ b/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
@@ -3,6 +3,7 @@ package PVE::API2::Network::SDN::RouteMaps::RouteMap;
use strict;
use warnings;
+use PVE::API2::Network::SDN::RouteMaps::RouteMapEntry;
use PVE::JSONSchema qw(get_standard_option);
use PVE::Exception qw(raise_param_exc);
use PVE::Tools qw(extract_param);
@@ -10,6 +11,11 @@ use PVE::Tools qw(extract_param);
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::RouteMaps::RouteMapEntry",
+ path => '{order}',
+});
+
__PACKAGE__->register_method({
name => 'list_route_map_entries',
path => '',
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm b/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
new file mode 100644
index 0000000..6967f3e
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
@@ -0,0 +1,136 @@
+package PVE::API2::Network::SDN::RouteMaps::RouteMapEntry;
+
+use strict;
+use warnings;
+
+use PVE::Exception qw(raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'get_route_map_entry',
+ path => '',
+ method => 'GET',
+ permissions => {
+ check => ['perm', '/sdn/route-maps/{route-map-id}', ['SDN.Audit', 'SDN.Allocate'], any => 1],
+ },
+ description => "Get Route Map Entry",
+ parameters => {
+ properties => {
+ 'route-map-id' => get_standard_option('pve-sdn-route-map-id'),
+ 'order' => get_standard_option('pve-sdn-route-map-order'),
+ },
+ },
+ returns => {
+ type => "object",
+ properties => PVE::Network::SDN::RouteMaps::route_map_properties(0),
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $route_map_id = extract_param($param, 'route-map-id');
+ my $order = extract_param($param, 'order');
+
+ my $route_map_entry = PVE::Network::SDN::RouteMaps::config()->get($route_map_id, $order);
+
+ raise_param_exc({ 'route-map-id' => "$route_map_id doesn't exist" })
+ if !$route_map_entry;
+
+ return $route_map_entry;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update_route_map_entry',
+ path => '',
+ method => 'PUT',
+ permissions => {
+ check => ['perm', '/sdn/route-maps/{route-map-id}', ['SDN.Allocate']],
+ },
+ description => "Update Route Map Entry",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ PVE::Network::SDN::RouteMaps::route_map_properties(1)->%*,
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::RouteMaps::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ my $route_map_id = extract_param($param, 'route-map-id');
+ my $order = extract_param($param, 'order');
+ my $delete = extract_param($param, 'delete');
+
+ $config->update($route_map_id, $order, $param);
+ PVE::Network::SDN::RouteMaps::write_config($config);
+ },
+ "updating route map entry failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'delete_route_map_entry',
+ path => '',
+ method => 'DELETE',
+ permissions => {
+ check => ['perm', '/sdn/route-maps/{route-map-id}', ['SDN.Allocate']],
+ },
+ description => "Delete Route Map Entry",
+ parameters => {
+ properties => {
+ digest => get_standard_option('pve-config-digest'),
+ 'lock-token' => get_standard_option('pve-sdn-lock-token'),
+ 'route-map-id' => get_standard_option('pve-sdn-route-map-id'),
+ 'order' => get_standard_option('pve-sdn-route-map-order'),
+ },
+ },
+ returns => {
+ type => "null",
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $lock_token = extract_param($param, 'lock-token');
+
+ PVE::Network::SDN::lock_sdn_config(
+ sub {
+ my $config = PVE::Network::SDN::RouteMaps::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ my $route_map_id = extract_param($param, 'route-map-id');
+ my $order = extract_param($param, 'order');
+
+ $config->delete($route_map_id, $order);
+ PVE::Network::SDN::RouteMaps::write_config($config);
+ },
+ "deleting route map entry failed",
+ $lock_token,
+ );
+
+ return;
+ },
+});
+
+1;
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 08/13] evpn controller: add route_map_{in,out} parameter
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (20 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 07/13] api2: add route map entry module Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-27 10:44 ` Hannes Laimer
2026-03-25 9:41 ` [PATCH pve-network 09/13] sdn: generate route map / prefix list configuration on sdn apply Stefan Hanreich
` (6 subsequent siblings)
28 siblings, 1 reply; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
This parameter allows overriding the default MAP_VTEP_{IN,OUT} route
maps by specifying a custom route map configured in route-maps.cfg.
This can be used for filtering incoming and outgoing routes, e.g. for
only advertising type-5 routes to external peers or only allow
importing routes with specific route targets.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 19 +++++++++++++------
src/PVE/Network/SDN/Controllers/Plugin.pm | 14 ++++++++++++++
2 files changed, 27 insertions(+), 6 deletions(-)
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index 3e643b1..d7b838b 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -45,6 +45,8 @@ sub options {
'asn' => { optional => 0 },
'peers' => { optional => 1 },
'fabric' => { optional => 1 },
+ 'route-map-in' => { optional => 1 },
+ 'route-map-out' => { optional => 1 },
};
}
@@ -153,23 +155,28 @@ sub generate_frr_config {
push @{ $bgp_router->{neighbor_groups} }, $neighbor_group;
+ my $route_map_in = $plugin_config->{'route-map-in'} // 'MAP_VTEP_IN';
+ my $route_map_out = $plugin_config->{'route-map-out'} // 'MAP_VTEP_OUT';
+
# 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',
+ route_map_in => $route_map_in,
+ route_map_out => $route_map_out,
}],
advertise_all_vni => 1,
};
$bgp_router->{address_families}->{l2vpn_evpn}->{autort_as} = $autortas if $autortas;
- my $routemap_in = { seq => 1, action => "permit" };
- my $routemap_out = { seq => 1, action => "permit" };
+ if ($route_map_in eq 'MAP_VTEP_IN' && !$config->{frr}->{routemaps}->{'MAP_VTEP_IN'}) {
+ push($config->{frr}->{routemaps}->{'MAP_VTEP_IN'}->@*, { seq => 1, action => "permit" });
+ }
- push($config->{frr}->{routemaps}->{'MAP_VTEP_IN'}->@*, $routemap_in);
- push($config->{frr}->{routemaps}->{'MAP_VTEP_OUT'}->@*, $routemap_out);
+ if ($route_map_out eq 'MAP_VTEP_OUT' && !$config->{frr}->{routemaps}->{'MAP_VTEP_OUT'}) {
+ push($config->{frr}->{routemaps}->{'MAP_VTEP_OUT'}->@*, { seq => 1, action => "permit" });
+ }
return $config;
}
diff --git a/src/PVE/Network/SDN/Controllers/Plugin.pm b/src/PVE/Network/SDN/Controllers/Plugin.pm
index d70e518..5f9f1ef 100644
--- a/src/PVE/Network/SDN/Controllers/Plugin.pm
+++ b/src/PVE/Network/SDN/Controllers/Plugin.pm
@@ -7,6 +7,8 @@ use PVE::Tools;
use PVE::JSONSchema;
use PVE::Cluster;
+use PVE::Network::SDN::RouteMaps;
+
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::SectionConfig);
@@ -51,6 +53,18 @@ my $defaultData = {
'pve-sdn-controller-id',
{ completion => \&PVE::Network::SDN::complete_sdn_controller },
),
+ 'route-map-in' => {
+ description => "Route Map that should be applied for incoming routes",
+ type => 'string',
+ format => 'pve-sdn-route-map-id',
+ optional => 1,
+ },
+ 'route-map-out' => {
+ description => "Route Map that should be applied for outgoing routes",
+ type => 'string',
+ format => 'pve-sdn-route-map-id',
+ optional => 1,
+ },
},
};
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 09/13] sdn: generate route map / prefix list configuration on sdn apply
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (21 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 08/13] evpn controller: add route_map_{in,out} parameter Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-27 10:47 ` Hannes Laimer
2026-03-25 9:41 ` [PATCH pve-network 10/13] tests: add simple route map test case Stefan Hanreich
` (5 subsequent siblings)
28 siblings, 1 reply; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Commit the newly introduced configuration files to the running
configuration when applying the SDN configuration, so the FRR config
generation logic can use them to generate the FRR configuration for
them.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN.pm | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 0bb36bf..93b6698 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -25,6 +25,8 @@ use PVE::Network::SDN::Subnets;
use PVE::Network::SDN::Dhcp;
use PVE::Network::SDN::Frr;
use PVE::Network::SDN::Fabrics;
+use PVE::Network::SDN::RouteMaps;
+use PVE::Network::SDN::PrefixLists;
my $RUNNING_CFG_FILENAME = "sdn/.running-config";
@@ -207,12 +209,16 @@ sub compile_running_cfg {
my $controllers_cfg = PVE::Network::SDN::Controllers::config();
my $subnets_cfg = PVE::Network::SDN::Subnets::config();
my $fabrics_cfg = PVE::Network::SDN::Fabrics::config();
+ my $route_maps_cfg = PVE::Network::SDN::RouteMaps::config();
+ my $prefix_lists_cfg = PVE::Network::SDN::PrefixLists::config();
my $vnets = { ids => $vnets_cfg->{ids} };
my $zones = { ids => $zones_cfg->{ids} };
my $controllers = { ids => $controllers_cfg->{ids} };
my $subnets = { ids => $subnets_cfg->{ids} };
my $fabrics = { ids => $fabrics_cfg->to_sections() };
+ my $route_maps = { ids => $route_maps_cfg->to_sections() };
+ my $prefix_lists = { ids => $prefix_lists_cfg->to_sections() };
$cfg = {
version => $version,
@@ -221,6 +227,8 @@ sub compile_running_cfg {
controllers => $controllers,
subnets => $subnets,
fabrics => $fabrics,
+ 'route-maps' => $route_maps,
+ 'prefix-lists' => $prefix_lists,
};
return $cfg;
@@ -425,9 +433,11 @@ configuration.
=cut
sub generate_frr_raw_config {
- my ($running_config, $fabric_config) = @_;
+ my ($running_config, $fabric_config, $route_map_config, $prefix_list_config) = @_;
$running_config = PVE::Network::SDN::running_config() if !$running_config;
+ $prefix_list_config = PVE::Network::SDN::PrefixLists::config(1) if !$prefix_list_config;
+ $route_map_config = PVE::Network::SDN::RouteMaps::config(1) if !$route_map_config;
$fabric_config = PVE::Network::SDN::Fabrics::config(1) if !$fabric_config;
my $frr_config = {};
@@ -438,7 +448,7 @@ sub generate_frr_raw_config {
my $nodename = PVE::INotify::nodename();
return PVE::RS::SDN::get_frr_raw_config(
- $frr_config->{'frr'}, $fabric_config, $nodename,
+ $frr_config->{'frr'}, $prefix_list_config, $route_map_config, $fabric_config, $nodename,
);
}
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 10/13] tests: add simple route map test case
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (22 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 09/13] sdn: generate route map / prefix list configuration on sdn apply Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 11/13] tests: add bgp evpn route map/prefix list testcase Stefan Hanreich
` (4 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
This testcase specifies two simple route maps, map-in and map-out, and
then overrides the default route maps in the EVPN controller with the
custom route maps.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../evpn/routemap/expected_controller_config | 60 ++++++++++++++++
.../evpn/routemap/expected_sdn_interfaces | 41 +++++++++++
src/test/zones/evpn/routemap/interfaces | 7 ++
src/test/zones/evpn/routemap/sdn_config | 70 +++++++++++++++++++
4 files changed, 178 insertions(+)
create mode 100644 src/test/zones/evpn/routemap/expected_controller_config
create mode 100644 src/test/zones/evpn/routemap/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/routemap/interfaces
create mode 100644 src/test/zones/evpn/routemap/sdn_config
diff --git a/src/test/zones/evpn/routemap/expected_controller_config b/src/test/zones/evpn/routemap/expected_controller_config
new file mode 100644
index 0000000..1adcd91
--- /dev/null
+++ b/src/test/zones/evpn/routemap/expected_controller_config
@@ -0,0 +1,60 @@
+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
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map map-in in
+ neighbor VTEP route-map map-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-in deny 5
+ set src 192.0.2.1
+exit
+!
+route-map map-in permit 123
+ match ip next-hop address 192.0.2.45
+ match metric 8347
+ match local-preference 8347
+ set ip next-hop 198.51.100.3
+ set local-preference 1234
+ set tag 999
+exit
+!
+route-map map-in deny 222
+ match ip next-hop address 192.0.2.45
+ match metric 8347
+ match local-preference 8347
+exit
+!
+route-map map-out permit 999
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/routemap/expected_sdn_interfaces b/src/test/zones/evpn/routemap/expected_sdn_interfaces
new file mode 100644
index 0000000..4cf13e0
--- /dev/null
+++ b/src/test/zones/evpn/routemap/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/routemap/interfaces b/src/test/zones/evpn/routemap/interfaces
new file mode 100644
index 0000000..66bb826
--- /dev/null
+++ b/src/test/zones/evpn/routemap/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/routemap/sdn_config b/src/test/zones/evpn/routemap/sdn_config
new file mode 100644
index 0000000..8201eaf
--- /dev/null
+++ b/src/test/zones/evpn/routemap/sdn_config
@@ -0,0 +1,70 @@
+{
+ 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",
+ 'route-map-in' => 'map-in', 'route-map-out' => 'map-out' } },
+ },
+
+ subnets => {
+ ids => { 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ }
+ }
+ },
+ 'route-maps' => {
+ ids => {
+ 'map-in_222' => {
+ id => 'map-in_222',
+ type => 'route-map-entry',
+ action => 'deny',
+ match => [
+ 'key=ip-next-hop-address,value=192.0.2.45',
+ 'key=metric,value=8347',
+ 'key=local-preference,value=8347',
+ ],
+ },
+ 'map-in_5' => {
+ id => 'map-in_5',
+ type => 'route-map-entry',
+ action => 'deny',
+ set => [
+ 'key=src,value=192.0.2.1'
+ ],
+ },
+ 'map-in_123' => {
+ id => 'map-in_123',
+ type => 'route-map-entry',
+ action => 'permit',
+ match => [
+ 'key=ip-next-hop-address,value=192.0.2.45',
+ 'key=metric,value=8347',
+ 'key=local-preference,value=8347',
+ ],
+ set => [
+ 'key=ip-next-hop,value=198.51.100.3',
+ 'key=local-preference,value=1234',
+ 'key=tag,value=999',
+ ],
+ },
+ 'map-out_999' => {
+ id => 'map-out_999',
+ type => 'route-map-entry',
+ action => 'permit',
+ }
+ }
+ }
+}
+
+
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 11/13] tests: add bgp evpn route map/prefix list testcase
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (23 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 10/13] tests: add simple route map test case Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 12/13] tests: add route map with prefix " Stefan Hanreich
` (3 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Uses a EVPN controller in conjunction with a BGP controller. The
testcases overrides the routemap for one direction in either
controller, but leaves the default incoming route map in the EVPN
controller. Additionally the route map utilizes a custom prefix list
in its matching logic.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../expected_controller_config | 76 ++++++++++++++++
.../expected_sdn_interfaces | 41 +++++++++
.../bgp_evpn_routemap_prefix_list/interfaces | 7 ++
.../bgp_evpn_routemap_prefix_list/sdn_config | 86 +++++++++++++++++++
4 files changed, 210 insertions(+)
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_controller_config
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/interfaces
create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/sdn_config
diff --git a/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_controller_config b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_controller_config
new file mode 100644
index 0000000..1c32221
--- /dev/null
+++ b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/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 65002
+ 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 external
+ 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
+ neighbor BGP route-map map-in in
+ exit-address-family
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map map-out out
+ advertise-all-vni
+ autort as 65000
+ exit-address-family
+exit
+!
+router bgp 65002 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+ !
+ address-family l2vpn evpn
+ route-target import 65000:1000
+ route-target export 65000:1000
+ exit-address-family
+exit
+!
+ip prefix-list some_list deny 192.0.2.0/24 le 25
+ip prefix-list some_list deny 198.51.100.0/25 le 26 ge 25
+ip prefix-list some_list seq 22 permit 203.0.113.0/24
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map map-in permit 9
+ match ip next-hop prefix-list some_list
+exit
+!
+route-map map-in permit 99
+ match ip next-hop prefix-list some_list
+ set src 192.0.2.1
+ set ip next-hop 192.0.2.100
+exit
+!
+route-map map-out permit 999
+ match ip next-hop prefix-list some_list
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_sdn_interfaces b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_sdn_interfaces
new file mode 100644
index 0000000..4cf13e0
--- /dev/null
+++ b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/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_evpn_routemap_prefix_list/interfaces b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/interfaces
new file mode 100644
index 0000000..66bb826
--- /dev/null
+++ b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/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_evpn_routemap_prefix_list/sdn_config b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/sdn_config
new file mode 100644
index 0000000..24ee624
--- /dev/null
+++ b/src/test/zones/evpn/bgp_evpn_routemap_prefix_list/sdn_config
@@ -0,0 +1,86 @@
+{
+ 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",
+ 'route-map-out' => 'map-out'
+ },
+ localhost => {
+ type => "bgp",
+ 'peers' => '192.168.0.252,192.168.0.253',
+ ebgp => "1",
+ asn => "65002",
+ node => "localhost",
+ 'route-map-in' => 'map-in'
+ },
+ },
+ },
+
+ subnets => {
+ ids => { 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ }
+ }
+ },
+ 'prefix-lists' => {
+ ids => {
+ 'some_list' => {
+ id => 'some_list',
+ type => 'prefix-list',
+ entries => [
+ 'action=deny,prefix=192.0.2.0/24,le=25',
+ 'action=deny,prefix=198.51.100.0/25,ge=25,le=26',
+ 'action=permit,prefix=203.0.113.0/24,seq=22',
+ ]
+ }
+ }
+ },
+ 'route-maps' => {
+ ids => {
+ 'map-in_99' => {
+ id => 'map-in_99',
+ type => 'route-map-entry',
+ action => 'permit',
+ match => [
+ 'key=ip-next-hop-prefix-list,value=some_list'
+ ],
+ set => [
+ 'key=src,value=192.0.2.1',
+ 'key=ip-next-hop,value=192.0.2.100'
+ ]
+ },
+ 'map-in_9' => {
+ id => 'map-in_9',
+ type => 'route-map-entry',
+ action => 'permit',
+ match => [
+ 'key=ip-next-hop-prefix-list,value=some_list'
+ ]
+ },
+ 'map-out_999' => {
+ id => 'map-out_999',
+ type => 'route-map-entry',
+ action => 'permit',
+ match => [
+ 'key=ip-next-hop-prefix-list,value=some_list'
+ ]
+ }
+ }
+ }
+}
+
+
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 12/13] tests: add route map with prefix list testcase
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (24 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 11/13] tests: add bgp evpn route map/prefix list testcase Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 13/13] bgp controller: allow configuring custom route maps Stefan Hanreich
` (2 subsequent siblings)
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
A simple test case creating a prefix list and then using that prefix
list inside a match statement in a route map. Then that route map is
used for filtering outgoing routes in an EVPN controller.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../expected_controller_config | 49 ++++++++++++++++
.../expected_sdn_interfaces | 41 +++++++++++++
.../evpn/routemap_prefix_list/interfaces | 7 +++
.../evpn/routemap_prefix_list/sdn_config | 58 +++++++++++++++++++
4 files changed, 155 insertions(+)
create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_controller_config
create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/routemap_prefix_list/interfaces
create mode 100644 src/test/zones/evpn/routemap_prefix_list/sdn_config
diff --git a/src/test/zones/evpn/routemap_prefix_list/expected_controller_config b/src/test/zones/evpn/routemap_prefix_list/expected_controller_config
new file mode 100644
index 0000000..bf5f20f
--- /dev/null
+++ b/src/test/zones/evpn/routemap_prefix_list/expected_controller_config
@@ -0,0 +1,49 @@
+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
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map map-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 some_list deny 192.0.2.0/24 le 25
+ip prefix-list some_list deny 198.51.100.0/25 le 26 ge 25
+ip prefix-list some_list seq 22 permit 203.0.113.0/24
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map map-out permit 999
+ match ip next-hop prefix-list some_list
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/routemap_prefix_list/expected_sdn_interfaces b/src/test/zones/evpn/routemap_prefix_list/expected_sdn_interfaces
new file mode 100644
index 0000000..4cf13e0
--- /dev/null
+++ b/src/test/zones/evpn/routemap_prefix_list/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/routemap_prefix_list/interfaces b/src/test/zones/evpn/routemap_prefix_list/interfaces
new file mode 100644
index 0000000..66bb826
--- /dev/null
+++ b/src/test/zones/evpn/routemap_prefix_list/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/routemap_prefix_list/sdn_config b/src/test/zones/evpn/routemap_prefix_list/sdn_config
new file mode 100644
index 0000000..8f1179a
--- /dev/null
+++ b/src/test/zones/evpn/routemap_prefix_list/sdn_config
@@ -0,0 +1,58 @@
+{
+ 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",
+ 'route-map-out' => 'map-out'
+ }
+ },
+ },
+
+ subnets => {
+ ids => { 'myzone-10.0.0.0-24' => {
+ 'type' => 'subnet',
+ 'vnet' => 'myvnet',
+ 'gateway' => '10.0.0.1',
+ }
+ }
+ },
+ 'prefix-lists' => {
+ ids => {
+ 'some_list' => {
+ id => 'some_list',
+ type => 'prefix-list',
+ entries => [
+ 'action=deny,prefix=192.0.2.0/24,le=25',
+ 'action=deny,prefix=198.51.100.0/25,ge=25,le=26',
+ 'action=permit,prefix=203.0.113.0/24,seq=22',
+ ]
+ }
+ }
+ },
+ 'route-maps' => {
+ ids => {
+ 'map-out_999' => {
+ id => 'map-out_999',
+ type => 'route-map-entry',
+ action => 'permit',
+ match => [
+ 'key=ip-next-hop-prefix-list,value=some_list'
+ ]
+ }
+ }
+ }
+}
+
+
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* [PATCH pve-network 13/13] bgp controller: allow configuring custom route maps
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (25 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 12/13] tests: add route map with prefix " Stefan Hanreich
@ 2026-03-25 9:41 ` Stefan Hanreich
2026-03-25 11:38 ` [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-03-27 10:17 ` Stefan Hanreich
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 9:41 UTC (permalink / raw)
To: pve-devel
Allows specifying custom route maps, as created in our SDN stack, to
the BGP controller. This can e.g. be used for only selectively
exporting routes from the default routing table via BGP instead of
having to export the whole routing table.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
index d54c9ec..11fc931 100644
--- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
@@ -52,6 +52,8 @@ sub options {
'ebgp' => { optional => 1 },
'ebgp-multihop' => { optional => 1 },
'loopback' => { optional => 1 },
+ 'route-map-in' => { optional => 1 },
+ 'route-map-out' => { optional => 1 },
};
}
@@ -111,12 +113,17 @@ sub generate_frr_config {
my $mask = Net::IP::ip_is_ipv6($ifaceip) ? "128" : "32";
my $af_key = "${ipversion}_unicast";
+ my $bgp_neighbor = {
+ name => "BGP",
+ soft_reconfiguration_inbound => 1,
+ };
+
+ $bgp_neighbor->{route_map_in} = $plugin_config->{'route-map-in'} if $plugin_config->{'route-map-in'};
+ $bgp_neighbor->{route_map_out} = $plugin_config->{'route-map-out'} if $plugin_config->{'route-map-out'};
+
$bgp_router->{address_families}->{$af_key} //= {
networks => [],
- neighbors => [{
- name => "BGP",
- soft_reconfiguration_inbound => 1,
- }],
+ neighbors => [$bgp_neighbor],
};
push @{ $bgp_router->{address_families}->{$af_key}->{networks} }, "$ifaceip/$mask"
--
2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (26 preceding siblings ...)
2026-03-25 9:41 ` [PATCH pve-network 13/13] bgp controller: allow configuring custom route maps Stefan Hanreich
@ 2026-03-25 11:38 ` Stefan Hanreich
2026-03-27 10:17 ` Stefan Hanreich
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-25 11:38 UTC (permalink / raw)
To: pve-devel
I forgot to add the dependencies in the cover letter, so here they are:
proxmox-frr depends on proxmox-frr-templates
proxmox-frr depends on proxmox-sdn-types
proxmox-ve-config depends on proxmox-sdn-types
proxmox-ve-config depends on proxmox-frr
proxmox-perl-rs depends on proxmox-ve-config
pve-network depends on proxmox-perl-rs
pve-network depends on pve-cluster
On 3/25/26 10:41 AM, Stefan Hanreich wrote:
> ## Introduction
>
> This patch adds support for managing route maps and prefix lists to the SDN
> stack. With this patch series, route maps can be applied to the BGP and EVPN
> controller for incoming / outgoing route filtering. There are currently some
> other features in development that would make use of route maps as well, namely:
>
> * VRF route leaking
> * Route Redistribution for Fabrics
>
> Prefix Lists can be used for matching inside route map match statements. They
> are implemented so they can be used inside route map match statements for now.
>
> ## Motivation
>
> There are a lot of use-cases for enabling users to create their own route-maps,
> which was currently only possible by utilizing frr.conf.local - which was clunky
> and prone to issues. Route maps can be used for filtering in/outgoing routes and
> modifiy them, so users could e.g. only selectively advertise some routes via BGP
> or only import certain EVPN routes from outside.
>
> It also allows us to programmatically manage route maps via the UI, e.g. for
> deeper EVPN integration in PDM. This opens up a lot of possibilities for
> new features.
>
>
> ## Configuration Format
>
> This patch series adds two new configuration files, route-maps.cfg and
> prefix-lists.cfg in /etc/pve/sdn.
>
> ### route-maps.cfg
>
> An example route map configuration looks as follows:
>
> route-map-entry: example_123
> action permit
> match key=vni,value=23487
> set key=tag,value=23487
>
> This would create the following FRR route map entry:
>
> route-map example permit 123
> match evpn vni 23487
> set tag 23487
>
> Every entry in route-maps.cfg maps to an entry in a route map. They are
> identified by their name as well as their ordering number. `example_123`
> specifies the 123th entry in the route map 'example'. The main reason for
> choosing this format is, that having a single section for one route-map would be
> quite unwieldy. It'd require some format like this, which is pretty awkward to
> handle / validate:
>
> route-map-entry: example
> action permit,seq=123
> match key=vni,value=23487,seq=123
> set key=tag,value=23487,seq=123
>
> From a UI POV editing singular route map entries seems better as well, and with
> the mapping of section entries to route map entries, a suitable API design
> follows quite naturally and easily maps to the respective section config
> entries, without too much data mangling required.
>
>
> ### prefix-lists.cfg
>
> An example prefix list configuration looks as follows:
>
> prefix-list: example-1
> entries action=permit,prefix=192.0.2.0/24
> entries action=permit,prefix=192.0.2.0/24,le=32
> entries action=permit,prefix=192.0.2.0/24,le=32,ge=24,seq=123
>
> This would create the following FRR prefix list:
>
> ip prefix-list example-1 permit 192.0.2.0/24
> ip prefix-list example-1 permit 192.0.2.0/24 le 32
> ip prefix-list example-1 seq 123 permit 192.0.2.0/24 le 32 ge 24
>
>
> ## API endpoints
>
> This patch series introduces the following API endpoints in the /cluster/sdn
> subfolder:
>
>
> ### Route Maps
>
> GET /route-maps - lists all route map entries
> GET /route-maps/<id> - lists all route map entries for the route map <id>
> GET /route-maps/<id>/<order> - gets the order'th entry in route map <id>
> POST /route-maps - creates a new route map entry
> PUT /route-maps/<id>/<order> - updates the order'th entry in route map <id>
> DELETE /route-maps/<id>/<order> - deletes the order'th entry in route map <id>
>
>
> ### Prefix Lists
>
> GET /prefix-lists - lists all prefix lists
> GET /prefix-lists/<id> - get prefix list <id>
> POST /prefix-lists - create a new prefix list
> PUT /prefix-lists/<id> - update prefix list <id>
> DELETE /prefix-lists/<id> - delete prefix list <id>
>
>
> ## Open questions
>
> How should we handle overriding the auto-generated route maps (e.g. in the EVPN
> controller) and prefix lists?
>
> Currently this patch series disallows creating any route map / prefix list that
> have the same name as PVE auto-generated ones via the API. They can be
> overridden by creating a new route map and then selecting it in the respective
> entity (e.g. via route-map-in in the EVPN controller). Pre-defined prefix-lists
> cannot currently be overridden, since this usually makes little sense, as they
> are used in the auto-generated route maps, which can be overriden anyway.
>
> pve-cluster:
>
> Stefan Hanreich (2):
> cfs: add 'sdn/route-maps.cfg' to observed files
> cfs: add 'sdn/prefix-lists.cfg' to observed files
>
> src/PVE/Cluster.pm | 2 ++
> src/pmxcfs/status.c | 2 ++
> 2 files changed, 4 insertions(+)
>
>
> proxmox-ve-rs:
>
> Stefan Hanreich (9):
> sdn-types: add common route-map helper types
> frr: implement routemap match/set statements via adjacent tagging
> frr: allow rendering prefix-lists/route-maps separately
> frr-templates: change route maps template to adapt to new types
> ve-config: add prefix list section config
> ve-config: frr: implement frr config generation for prefix lists
> ve-config: add route map section config
> ve-config: frr: implement frr config generation for route maps
> ve-config: fabrics: adapt frr config generation to new format
>
> .../templates/route_maps.jinja | 12 +-
> proxmox-frr/Cargo.toml | 2 +-
> proxmox-frr/src/ser/route_map.rs | 101 ++-
> proxmox-frr/src/ser/serializer.rs | 35 +-
> proxmox-sdn-types/src/bgp.rs | 50 ++
> proxmox-sdn-types/src/lib.rs | 135 ++++
> proxmox-ve-config/debian/control | 2 +
> proxmox-ve-config/src/sdn/fabric/frr.rs | 25 +-
> proxmox-ve-config/src/sdn/mod.rs | 2 +
> proxmox-ve-config/src/sdn/prefix_list.rs | 347 ++++++++
> proxmox-ve-config/src/sdn/route_map.rs | 762 ++++++++++++++++++
> 11 files changed, 1408 insertions(+), 65 deletions(-)
> create mode 100644 proxmox-sdn-types/src/bgp.rs
> create mode 100644 proxmox-ve-config/src/sdn/prefix_list.rs
> create mode 100644 proxmox-ve-config/src/sdn/route_map.rs
>
>
> proxmox-perl-rs:
>
> Stefan Hanreich (3):
> pve-rs: sdn: add route maps module
> pve-rs: sdn: add prefix lists module
> sdn: add prefix list / route maps to frr config generation helper
>
> pve-rs/Cargo.toml | 1 +
> pve-rs/Makefile | 2 +
> pve-rs/src/bindings/sdn/mod.rs | 30 ++-
> pve-rs/src/bindings/sdn/prefix_lists.rs | 199 +++++++++++++++++++
> pve-rs/src/bindings/sdn/route_maps.rs | 243 ++++++++++++++++++++++++
> 5 files changed, 472 insertions(+), 3 deletions(-)
> create mode 100644 pve-rs/src/bindings/sdn/prefix_lists.rs
> create mode 100644 pve-rs/src/bindings/sdn/route_maps.rs
>
>
> pve-network:
>
> Stefan Hanreich (13):
> controller: bgp: evpn: adapt to new match / set frr config syntax
> sdn: add prefix lists module
> api2: add prefix list module
> sdn: add route map module
> api2: add route maps api module
> api2: add route map module
> api2: add route map entry module
> evpn controller: add route_map_{in,out} parameter
> sdn: generate route map / prefix list configuration on sdn apply
> tests: add simple route map test case
> tests: add bgp evpn route map/prefix list testcase
> tests: add route map with prefix list testcase
> bgp controller: allow configuring custom route maps
>
> src/PVE/API2/Network/SDN.pm | 14 +
> src/PVE/API2/Network/SDN/Makefile | 13 +-
> src/PVE/API2/Network/SDN/PrefixLists.pm | 247 ++++++++++++++++++
> src/PVE/API2/Network/SDN/RouteMaps.pm | 137 ++++++++++
> src/PVE/API2/Network/SDN/RouteMaps/Makefile | 9 +
> .../API2/Network/SDN/RouteMaps/RouteMap.pm | 92 +++++++
> .../Network/SDN/RouteMaps/RouteMapEntry.pm | 136 ++++++++++
> src/PVE/Network/SDN.pm | 14 +-
> src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 22 +-
> src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 43 +--
> src/PVE/Network/SDN/Controllers/Plugin.pm | 14 +
> src/PVE/Network/SDN/Makefile | 14 +-
> src/PVE/Network/SDN/PrefixLists.pm | 134 ++++++++++
> src/PVE/Network/SDN/RouteMaps.pm | 173 ++++++++++++
> .../expected_controller_config | 76 ++++++
> .../expected_sdn_interfaces | 41 +++
> .../bgp_evpn_routemap_prefix_list/interfaces | 7 +
> .../bgp_evpn_routemap_prefix_list/sdn_config | 86 ++++++
> .../evpn/routemap/expected_controller_config | 60 +++++
> .../evpn/routemap/expected_sdn_interfaces | 41 +++
> src/test/zones/evpn/routemap/interfaces | 7 +
> src/test/zones/evpn/routemap/sdn_config | 70 +++++
> .../expected_controller_config | 49 ++++
> .../expected_sdn_interfaces | 41 +++
> .../evpn/routemap_prefix_list/interfaces | 7 +
> .../evpn/routemap_prefix_list/sdn_config | 58 ++++
> 26 files changed, 1572 insertions(+), 33 deletions(-)
> create mode 100644 src/PVE/API2/Network/SDN/PrefixLists.pm
> create mode 100644 src/PVE/API2/Network/SDN/RouteMaps.pm
> create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/Makefile
> create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
> create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
> create mode 100644 src/PVE/Network/SDN/PrefixLists.pm
> create mode 100644 src/PVE/Network/SDN/RouteMaps.pm
> create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_controller_config
> create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_sdn_interfaces
> create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/interfaces
> create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/sdn_config
> create mode 100644 src/test/zones/evpn/routemap/expected_controller_config
> create mode 100644 src/test/zones/evpn/routemap/expected_sdn_interfaces
> create mode 100644 src/test/zones/evpn/routemap/interfaces
> create mode 100644 src/test/zones/evpn/routemap/sdn_config
> create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_controller_config
> create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_sdn_interfaces
> create mode 100644 src/test/zones/evpn/routemap_prefix_list/interfaces
> create mode 100644 src/test/zones/evpn/routemap_prefix_list/sdn_config
>
>
> Summary over all repositories:
> 44 files changed, 3456 insertions(+), 101 deletions(-)
>
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 3/9] frr: allow rendering prefix-lists/route-maps separately
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 3/9] frr: allow rendering prefix-lists/route-maps separately Stefan Hanreich
@ 2026-03-25 14:32 ` Gabriel Goller
2026-03-26 12:17 ` Stefan Hanreich
2026-03-27 10:50 ` Hannes Laimer
1 sibling, 1 reply; 62+ messages in thread
From: Gabriel Goller @ 2026-03-25 14:32 UTC (permalink / raw)
To: Stefan Hanreich; +Cc: pve-devel
On 25.03.2026 10:41, Stefan Hanreich wrote:
> This is mainly useful for integration tests for the route maps /
> prefix lists feature in ve-config. It utilizes those methods to render
> route maps and prefix lists in tests, without having to render a full
> FRR configuration.
Why can't we move the inline tests into a tests/ dir (maybe in ve-config for now)
and just generate the whole config (using Default we should be able to just omit
the rest of the config)? IMO this is a bit weird having these two test-helpers
here. This also doesn't catch errors importing the jinja template.
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> proxmox-frr/src/ser/serializer.rs | 35 ++++++++++++++++++++++++++++++-
> 1 file changed, 34 insertions(+), 1 deletion(-)
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 4/9] frr-templates: change route maps template to adapt to new types
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 4/9] frr-templates: change route maps template to adapt to new types Stefan Hanreich
@ 2026-03-25 14:33 ` Gabriel Goller
2026-03-25 14:58 ` Gabriel Goller
2026-03-27 11:01 ` Hannes Laimer
1 sibling, 1 reply; 62+ messages in thread
From: Gabriel Goller @ 2026-03-25 14:33 UTC (permalink / raw)
To: Stefan Hanreich; +Cc: pve-devel
On 25.03.2026 10:41, Stefan Hanreich wrote:
> Instead of defining every potential match / set type manually under a
> different name, proxmox-frr now uses the Adjacently tagged
> representation for representing key/value pairs for match/set actions.
> This allows simplifying the route_maps template by simply rendering
> the respective key / value fields.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> proxmox-frr-templates/templates/route_maps.jinja | 12 ++++--------
> 1 file changed, 4 insertions(+), 8 deletions(-)
>
> diff --git a/proxmox-frr-templates/templates/route_maps.jinja b/proxmox-frr-templates/templates/route_maps.jinja
> index 172c682..19a6ee4 100644
> --- a/proxmox-frr-templates/templates/route_maps.jinja
> +++ b/proxmox-frr-templates/templates/route_maps.jinja
> @@ -3,16 +3,12 @@
> !
> 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 }}
> -{% elif match.match_type == "next-hop" %}
> - match {{ match.protocol_type }} next-hop {{ match.value }}
> -{% endif %}
> + match {{ match.key }} {% if match.value is defined %} {{ match.value }} {% endif %}
> +
Why is there a newline here?
IMO if we add this it should be after the {% endfor %} as a separation between
`match` and `set` statements.
> {% endfor %}
> {% for set in routemap.sets %}
> - set {{ set.set_type }} {{ set.value }}
> + set {{ set.key }} {% if set.value is defined %} {{ set.value }} {% endif %}
> +
And here?
> {% endfor %}
> {% for line in routemap.custom_frr_config %}
> {{ line }}
> --
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 7/9] ve-config: add route map section config
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 7/9] ve-config: add route map section config Stefan Hanreich
@ 2026-03-25 14:35 ` Gabriel Goller
2026-03-26 13:49 ` Stefan Hanreich
0 siblings, 1 reply; 62+ messages in thread
From: Gabriel Goller @ 2026-03-25 14:35 UTC (permalink / raw)
To: Stefan Hanreich; +Cc: pve-devel
On 25.03.2026 10:41, Stefan Hanreich wrote:
> Those types represent FRR route maps inside a section config format.
> For an example of the exact format and its FRR representation see the
> module-level documentation.
>
> One section config entry maps to one route map entry. A route map
> consists of one or more route map entries inside the section config.
> The ID of a section encodes the name of the route map as well as the
> order # of the entry.
>
> The route map module exports specific types for the API that handle
> converting the section config ID, because currently it is only
> possible to deserialize section config IDs to Strings. To avoid
> having to implement the parsing logic along every step of the stack
> (Perl backend, UI), use specific API types in the public API that
> handle parsing the section ID into route map name and order.
>
> Contrary to most SDN entities, route maps IDs can be 32 characters
> long instead of 8 and support underscores as well as hyphens. This is
> because the restriction of having to generate network interface names
> does not apply to FRR entities, so we can be more lenient with IDs
> here, allowing users to specify more descriptive names.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> proxmox-ve-config/debian/control | 2 +
> proxmox-ve-config/src/sdn/mod.rs | 1 +
> proxmox-ve-config/src/sdn/route_map.rs | 491 +++++++++++++++++++++++++
> 3 files changed, 494 insertions(+)
> create mode 100644 proxmox-ve-config/src/sdn/route_map.rs
>
> diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
> index 440cf73..5206340 100644
> --- a/proxmox-ve-config/debian/control
> +++ b/proxmox-ve-config/debian/control
> @@ -24,6 +24,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
> librust-serde-1+default-dev <!nocheck>,
> librust-serde-1+derive-dev <!nocheck>,
> librust-serde-json-1+default-dev <!nocheck>,
> + librust-serde-with-3+default-dev <!nocheck>,
> librust-thiserror-2+default-dev <!nocheck>,
> librust-tracing-0.1+default-dev (>= 0.1.37-~~) <!nocheck>
> Maintainer: Proxmox Support Team <support@proxmox.com>
> @@ -55,6 +56,7 @@ Depends:
> librust-serde-1+default-dev,
> librust-serde-1+derive-dev,
> librust-serde-json-1+default-dev,
> + librust-serde-with-3+default-dev,
> librust-thiserror-2+default-dev,
> librust-tracing-0.1+default-dev (>= 0.1.37-~~)
> Suggests:
serde-with is not import and used AFAICS.
> diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
> index 344c02c..24069ad 100644
> --- a/proxmox-ve-config/src/sdn/mod.rs
> +++ b/proxmox-ve-config/src/sdn/mod.rs
> @@ -2,6 +2,7 @@ pub mod config;
> pub mod fabric;
> pub mod ipam;
> pub mod prefix_list;
> +pub mod route_map;
>
> use std::{error::Error, fmt::Display, str::FromStr};
>
> diff --git a/proxmox-ve-config/src/sdn/route_map.rs b/proxmox-ve-config/src/sdn/route_map.rs
> new file mode 100644
> index 0000000..3f4da56
> --- /dev/null
> +++ b/proxmox-ve-config/src/sdn/route_map.rs
> @@ -0,0 +1,491 @@
> +//! Section config types for FRR Route Maps.
> +//!
> +//! This module contains the API types required for representing FRR Route Maps as section config.
> +//! Each entry in the section config maps to a Route Map entry, *not* a route map as a whole, the
> +//! order of the entry is encoded in the ID of the Route Map.
> +//!
> +//! Route maps in FRR consists of at least one entry, which are ordered by their given sequence
> +//! number / order. Each entry has a default matching policy, which is applied if the matching
> +//! conditions of the entry are met.
> +//!
> +//! An example for a simple FRR Route Map entry loooks like this:
> +//!
> +//! ```text
> +//! route-map test permit 10
> +//! match ip next-hop address 192.0.2.1
> +//! set local-preference 200
> +//! ```
> +//!
> +//! The corresponding representation as a section config entry looks like this:
> +//!
> +//! ```text
> +//! route-map-entry: test_10
> +//! action permit
> +//! match key=ip-next-hop-address,value=192.0.2.1
> +//! set key=local-preference,value=200
> +//! ```
> +//!
> +//! Match and Set Actions are encoded as an array with a property string that has a key and an
> +//! optional value paramter, because some options do not require an additional value.
> +//!
> +//! This abstraction currently supports Match and Set actions, but not call actions and exit
> +//! actions.
> +
> +use core::net::IpAddr;
We usually always use imports from std:: (also in libraries). Note that this
series introduces core:: imports in a few ve-rs patches.
> +
> +use anyhow::format_err;
> +use const_format::concatcp;
> +
> +use proxmox_network_types::ip_address::api_types::{Ipv4Addr, Ipv6Addr};
> +use proxmox_sdn_types::{
> + bgp::{EvpnRouteType, SetMetricValue, SetTagValue},
> + IntegerWithSign, Vni,
> [snip]
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 4/9] frr-templates: change route maps template to adapt to new types
2026-03-25 14:33 ` Gabriel Goller
@ 2026-03-25 14:58 ` Gabriel Goller
0 siblings, 0 replies; 62+ messages in thread
From: Gabriel Goller @ 2026-03-25 14:58 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
On 25.03.2026 15:33, Gabriel Goller wrote:
> On 25.03.2026 10:41, Stefan Hanreich wrote:
> > Instead of defining every potential match / set type manually under a
> > different name, proxmox-frr now uses the Adjacently tagged
> > representation for representing key/value pairs for match/set actions.
> > This allows simplifying the route_maps template by simply rendering
> > the respective key / value fields.
> >
> > Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> > ---
> > proxmox-frr-templates/templates/route_maps.jinja | 12 ++++--------
> > 1 file changed, 4 insertions(+), 8 deletions(-)
> >
> > diff --git a/proxmox-frr-templates/templates/route_maps.jinja b/proxmox-frr-templates/templates/route_maps.jinja
> > index 172c682..19a6ee4 100644
> > --- a/proxmox-frr-templates/templates/route_maps.jinja
> > +++ b/proxmox-frr-templates/templates/route_maps.jinja
> > @@ -3,16 +3,12 @@
> > !
> > 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 }}
> > -{% elif match.match_type == "next-hop" %}
> > - match {{ match.protocol_type }} next-hop {{ match.value }}
> > -{% endif %}
> > + match {{ match.key }} {% if match.value is defined %} {{ match.value }} {% endif %}
> > +
>
> Why is there a newline here?
> IMO if we add this it should be after the {% endfor %} as a separation between
> `match` and `set` statements.
Ah, nevermind, these are here because the line above neds with a statement (the
inline-if) and thus no newline is added. This could be added to the commit msg
maybe?
> > {% endfor %}
> > {% for set in routemap.sets %}
> > - set {{ set.set_type }} {{ set.value }}
> > + set {{ set.key }} {% if set.value is defined %} {{ set.value }} {% endif %}
> > +
>
> And here?
>
> > {% endfor %}
> > {% for line in routemap.custom_frr_config %}
> > {{ line }}
> > --
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 8/9] ve-config: frr: implement frr config generation for route maps
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 8/9] ve-config: frr: implement frr config generation for route maps Stefan Hanreich
@ 2026-03-25 15:03 ` Gabriel Goller
2026-03-26 13:50 ` Stefan Hanreich
2026-03-27 11:17 ` Hannes Laimer
1 sibling, 1 reply; 62+ messages in thread
From: Gabriel Goller @ 2026-03-25 15:03 UTC (permalink / raw)
To: Stefan Hanreich; +Cc: pve-devel
On 25.03.2026 10:41, Stefan Hanreich wrote:
> Implements conversion traits for all the section config types, so they
> can be converted into their respective FRR template counterpart.
>
> This module contains a helper for adding all route map entries to an
> existing FRR configuration. It will overwrite existing route map
> entries that have the same name AND order number. But if entries with
> the same name, but different ordering, exist they will only be added
> to the existing FRR configuration without dropping the other route map
> entries.
> This currently not relevant either way, because the initial API
> implementation will reject creating route maps with names of route
> maps that the stack auto-generates. In the future this behavior can
> be used for selectively overriding / appending existing Proxmox VE
> route maps.
>
> The helper also automatically orders route map entries according to
> their ordering number. This allows for deterministic FRR configuration
> output, which is important for tests and convenient for human
> readability.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> proxmox-ve-config/src/sdn/route_map.rs | 271 +++++++++++++++++++++++++
> 1 file changed, 271 insertions(+)
>
> diff --git a/proxmox-ve-config/src/sdn/route_map.rs b/proxmox-ve-config/src/sdn/route_map.rs
> index 3f4da56..8d8c4dc 100644
> --- a/proxmox-ve-config/src/sdn/route_map.rs
> +++ b/proxmox-ve-config/src/sdn/route_map.rs
[snip]
> + impl Into<RouteMapMatch> for MatchAction {
> + fn into(self) -> RouteMapMatch {
> + match self {
> + Self::RouteType(evpn_route_type) => RouteMapMatch::RouteType(evpn_route_type),
> + Self::Vni(vni) => RouteMapMatch::Vni(vni),
> + Self::IpAddressPrefixList(prefix_list_name) => {
> + RouteMapMatch::IpAddressPrefixList(prefix_list_name.into())
> + }
> + Self::Ip6AddressPrefixList(prefix_list_name) => {
> + RouteMapMatch::Ip6AddressPrefixList(prefix_list_name.into())
> + }
> + Self::IpNextHopPrefixList(prefix_list_name) => {
> + RouteMapMatch::IpNextHopPrefixList(prefix_list_name.into())
> + }
> + Self::Ip6NextHopPrefixList(prefix_list_name) => {
> + RouteMapMatch::Ip6NextHopPrefixList(prefix_list_name.into())
> + }
> + Self::IpNextHopAddress(ipv4_addr) => RouteMapMatch::IpNextHopAddress(*ipv4_addr),
> + Self::Ip6NextHopAddress(ipv6_addr) => RouteMapMatch::Ip6NextHopAddress(*ipv6_addr),
> + Self::Metric(metric) => RouteMapMatch::Metric(metric),
> + Self::LocalPreference(local_preference) => {
> + RouteMapMatch::LocalPreference(local_preference)
> + }
> + Self::Peer(ip_addr) => RouteMapMatch::Peer(ip_addr),
> + Self::Tag(tag) => RouteMapMatch::Tag(tag),
> + }
> + }
> + }
> +
> + impl Into<RouteMapSet> for SetAction {
> + fn into(self) -> RouteMapSet {
> + match self {
> + Self::IpNextHopPeerAddress => RouteMapSet::IpNextHopPeerAddress,
> + Self::IpNextHopUnchanged => RouteMapSet::IpNextHopUnchanged,
> + Self::IpNextHop(ipv4_addr) => RouteMapSet::IpNextHop(*ipv4_addr),
> + Self::Ip6NextHopPeerAddress => RouteMapSet::Ip6NextHopPeerAddress,
> + Self::Ip6NextHopPreferGlobal => RouteMapSet::Ip6NextHopPreferGlobal,
> + Self::Ip6NextHop(ipv6_addr) => RouteMapSet::Ip6NextHop(*ipv6_addr),
> + Self::LocalPreference(local_preference) => {
> + RouteMapSet::LocalPreference(local_preference)
> + }
> + Self::Tag(tag) => RouteMapSet::Tag(tag),
> + Self::Weight(weight) => RouteMapSet::Weight(weight),
> + Self::Metric(metric) => RouteMapSet::Metric(metric),
> + Self::Src(src) => RouteMapSet::Src(src),
> + }
> + }
> + }
> +
> + impl Into<FrrRouteMap> for RouteMapEntry {
> + fn into(self) -> FrrRouteMap {
> + FrrRouteMap {
> + seq: self.id.order,
> + action: match self.action {
> + RouteMapAction::Permit => proxmox_frr::ser::route_map::AccessAction::Permit,
> + RouteMapAction::Deny => proxmox_frr::ser::route_map::AccessAction::Deny,
> + },
> + matches: self
> + .match_actions
> + .into_iter()
> + .map(|match_action| match_action.into_inner().into())
> + .collect(),
> + sets: self
> + .set_actions
> + .into_iter()
> + .map(|set_action| set_action.into_inner().into())
> + .collect(),
> + custom_frr_config: Default::default(),
> + }
> + }
> + }
These impls above could be From<>, even though we never need the other direction
:) clippy otherwise complains.
> +
> + /// Add a list of Route Map Entries to a [`FrrConfig`].
> + ///
> + /// This method takes a list of Route Map Entries and adds them to given FRR configuration.
> + /// Existing Route Map entries with the same name, but different ordering number will remain in
> + /// the configuration. Entries with the same ordering will get merged.
> + ///
> + /// This behavior is different from Prefix Lists, where we overwrite existing Prefix Lists in
> + /// the FRR configuration. The reason for this is that users can override the Route Map setting
> + /// in the EVPN controller.
> + pub fn build_frr_route_maps(
> + config: impl IntoIterator<Item = RouteMap>,
> + frr_config: &mut FrrConfig,
> + ) -> Result<(), anyhow::Error> {
> + for route_map in config.into_iter() {
> + let RouteMap::RouteMapEntry(route_map) = route_map;
> + let route_map_name = RouteMapName::new(route_map.id.route_map_id.to_string());
> +
> + if let Some(frr_route_map) = frr_config.routemaps.get_mut(&route_map_name) {
> + let idx =
> + frr_route_map.partition_point(|element| element.seq <= route_map.id().order());
> + frr_route_map.insert(idx, route_map.into());
> + } else {
> + frr_config
> + .routemaps
> + .insert(route_map_name, vec![route_map.into()]);
> + }
> + }
> +
> + Ok(())
> + }
> +
> [snip]
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-perl-rs 1/3] pve-rs: sdn: add route maps module
2026-03-25 9:41 ` [PATCH proxmox-perl-rs 1/3] pve-rs: sdn: add route maps module Stefan Hanreich
@ 2026-03-26 10:32 ` Wolfgang Bumiller
2026-03-26 13:57 ` Stefan Hanreich
0 siblings, 1 reply; 62+ messages in thread
From: Wolfgang Bumiller @ 2026-03-26 10:32 UTC (permalink / raw)
To: Stefan Hanreich; +Cc: pve-devel
On Wed, Mar 25, 2026 at 10:41:23AM +0100, Stefan Hanreich wrote:
> Exposes the functionality from ve-config to Perl by providing helpers
> for instantiating the Rust configuration from Perl. The module also
> contains the implementation for the CRUD API methods, which will be
> used in the API methods in pve-network.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> pve-rs/Cargo.toml | 1 +
> pve-rs/Makefile | 1 +
> pve-rs/src/bindings/sdn/mod.rs | 3 +-
> pve-rs/src/bindings/sdn/route_maps.rs | 243 ++++++++++++++++++++++++++
> 4 files changed, 247 insertions(+), 1 deletion(-)
> create mode 100644 pve-rs/src/bindings/sdn/route_maps.rs
>
> diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
> index 45389b5..42d19fe 100644
> --- a/pve-rs/Cargo.toml
> +++ b/pve-rs/Cargo.toml
> @@ -42,6 +42,7 @@ proxmox-notify = { version = "1", features = ["pve-context"] }
> proxmox-oci = "0.2.1"
> proxmox-openid = "1.0.2"
> proxmox-resource-scheduling = "1.0.1"
> +proxmox-schema = "5"
> proxmox-section-config = "3"
> proxmox-shared-cache = "1"
> proxmox-subscription = "1"
> diff --git a/pve-rs/Makefile b/pve-rs/Makefile
> index 3bbc464..d662b00 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::RouteMaps \
> PVE::RS::SDN \
> PVE::RS::TFA
>
> diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs
> index fde3138..c571d28 100644
> --- a/pve-rs/src/bindings/sdn/mod.rs
> +++ b/pve-rs/src/bindings/sdn/mod.rs
> @@ -1,4 +1,5 @@
> pub(crate) mod fabrics;
> +pub(crate) mod route_maps;
>
> #[perlmod::package(name = "PVE::RS::SDN", lib = "pve_rs")]
> pub mod pve_rs_sdn {
> @@ -7,7 +8,7 @@ pub mod pve_rs_sdn {
> //! This provides general methods for generating the frr config.
>
> use anyhow::Error;
> - use proxmox_frr::ser::{FrrConfig, serializer::to_raw_config};
> + use proxmox_frr::ser::{serializer::to_raw_config, FrrConfig};
>
> use proxmox_ve_config::common::valid::Validatable;
> use proxmox_ve_config::sdn::fabric::section_config::node::NodeId;
> diff --git a/pve-rs/src/bindings/sdn/route_maps.rs b/pve-rs/src/bindings/sdn/route_maps.rs
> new file mode 100644
> index 0000000..b80126a
> --- /dev/null
> +++ b/pve-rs/src/bindings/sdn/route_maps.rs
> @@ -0,0 +1,243 @@
> +#[perlmod::package(name = "PVE::RS::SDN::RouteMaps", lib = "pve_rs")]
> +pub mod pve_rs_sdn_route_maps {
> + //! The `PVE::RS::SDN::RouteMaps` package.
> +
> + use std::collections::HashMap;
> + use std::ops::Deref;
> + use std::sync::Mutex;
> +
> + use anyhow::{anyhow, Error};
> + use openssl::hash::{hash, MessageDigest};
> + use serde::{Deserialize, Serialize};
> +
> + use perlmod::Value;
> +
> + use proxmox_schema::Updater;
> + use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
> + use proxmox_ve_config::sdn::route_map::api::RouteMapDeletableProperties;
> + use proxmox_ve_config::sdn::route_map::api::RouteMapEntry as ApiRouteMap;
> + use proxmox_ve_config::sdn::route_map::api::RouteMapEntryUpdater;
> + use proxmox_ve_config::sdn::route_map::RouteMap as ConfigRouteMap;
> + use proxmox_ve_config::sdn::route_map::RouteMapEntryId;
> + use proxmox_ve_config::sdn::route_map::RouteMapId;
> +
> + /// A SDN RouteMap config instance.
> + #[derive(Serialize, Deserialize)]
> + pub struct PerlRouteMapConfig {
> + /// The route map config instance
> + pub route_maps: Mutex<HashMap<String, ConfigRouteMap>>,
> + }
> +
> + perlmod::declare_magic!(Box<PerlRouteMapConfig> : &PerlRouteMapConfig as "PVE::RS::SDN::RouteMaps::Config");
> +
> + /// Class method: Parse the raw configuration from `/etc/pve/sdn/route-maps.cfg`.
> + #[export]
> + pub fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, Error> {
> + let raw_config = std::str::from_utf8(raw_config)?;
> + let config = ConfigRouteMap::parse_section_config("route-maps.cfg", raw_config)?;
> +
> + Ok(
> + perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlRouteMapConfig {
> + route_maps: Mutex::new(config.deref().clone()),
> + })),
> + )
> + }
> +
> + /// Class method: Parse the configuration from `/etc/pve/sdn/.running_config`.
> + #[export]
> + pub fn running_config(
> + #[raw] class: Value,
> + route_maps: HashMap<String, ConfigRouteMap>,
> + ) -> Result<perlmod::Value, Error> {
> + Ok(
> + perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlRouteMapConfig {
> + route_maps: Mutex::new(route_maps.clone()),
> + })),
> + )
> + }
> +
> + /// Used for writing the running configuration.
> + #[export]
> + pub fn to_sections(
> + #[try_from_ref] this: &PerlRouteMapConfig,
> + ) -> Result<HashMap<String, ConfigRouteMap>, Error> {
> + let config = this.route_maps.lock().unwrap();
> + Ok(config.deref().clone())
> + }
> +
> + /// Method: Convert the configuration into the section config string.
> + ///
> + /// Used for writing `/etc/pve/sdn/route-maps.cfg`
> + #[export]
> + pub fn to_raw(#[try_from_ref] this: &PerlRouteMapConfig) -> Result<String, Error> {
> + let config = this.route_maps.lock().unwrap();
> + let route_maps: SectionConfigData<ConfigRouteMap> =
> + SectionConfigData::from_iter(config.deref().clone());
> +
> + ConfigRouteMap::write_section_config("route-maps.cfg", &route_maps)
> + }
> +
> + /// Method: Generate a digest for the whole configuration.
> + #[export]
> + pub fn digest(#[try_from_ref] this: &PerlRouteMapConfig) -> Result<String, Error> {
> + let config = to_raw(this)?;
> + let hash = hash(MessageDigest::sha256(), config.as_bytes())?;
> +
> + Ok(hex::encode(hash))
> + }
> +
> + /// Returns a list of all RouteMap entries.
^ Method:
> + #[export]
> + pub fn list(
> + #[try_from_ref] this: &PerlRouteMapConfig,
> + ) -> Result<HashMap<String, ApiRouteMap>, Error> {
> + Ok(this
> + .route_maps
> + .lock()
> + .unwrap()
> + .iter()
> + .map(|(id, route_map_entry)| {
> + let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
> + (id.clone(), route_map.clone().into())
> + })
> + .collect())
> + }
> +
> + /// Returns a list of all RouteMap entries for a given RouteMap ID.
^ Method:
and it doesn't return a list - which in perl is quite specific.
(Note that perlmod gained support for returning a *list* via
`perlmod::ser::Return`'s `List(T)` variant, as well as a `@`-like final
list parameter via the `#[list]` attribute. Also note that the latter is
not meant to be used for anything other than implementing a pre-existing
*perl* API)
> + #[export]
> + pub fn list_route_map(
> + #[try_from_ref] this: &PerlRouteMapConfig,
> + route_map_id: RouteMapId,
> + ) -> Result<HashMap<String, ApiRouteMap>, Error> {
> + Ok(this
> + .route_maps
> + .lock()
> + .unwrap()
> + .iter()
> + .filter_map(|(id, route_map_entry)| {
> + let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
> +
> + if route_map.id().route_map_id() == &route_map_id {
> + return Some((id.clone(), route_map.clone().into()));
> + }
> +
> + None
> + })
> + .collect())
> + }
> +
> + /// Create a new RouteMap entry.
^ more missing `Method:` annotations follow...
> + #[export]
> + pub fn create(
> + #[try_from_ref] this: &PerlRouteMapConfig,
> + route_map: ApiRouteMap,
> + ) -> Result<(), Error> {
> + let mut route_maps = this.route_maps.lock().unwrap();
> +
> + let id =
> + RouteMapEntryId::new(route_map.route_map_id().clone(), route_map.order()).to_string();
So the key we use in `route_maps` is constructed from the route_map's id
and order...
> + let config_route_map = ConfigRouteMap::RouteMapEntry(route_map.into());
> +
> + if route_maps.get(&id).is_some() {
> + anyhow::bail!("route map entry already exists in configuration: {}", id);
> + }
> +
> + route_maps.insert(id, config_route_map);
^ The above two should probably use the entry api
route_maps.entry() {
Entry::Occupied(_) => bail!(...),
Entry::Vacant(vacancy) => vacancy.insert(...),
}
> +
> + Ok(())
> + }
> +
> + /// Returns a specfic entry of a RouteMap.
> + #[export]
> + pub fn get(
> + #[try_from_ref] this: &PerlRouteMapConfig,
> + route_map_id: RouteMapId,
> + order: u32,
> + ) -> Result<Option<ApiRouteMap>, Error> {
> + let id = RouteMapEntryId::new(route_map_id, order);
> +
> + Ok(this
> + .route_maps
> + .lock()
> + .unwrap()
> + .iter()
> + .find(|(_id, route_map_entry)| {
> + let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
> + route_map.id() == &id
...so could we just `.get()` with a `RouteMapEntryId::new(route_map_id,
order).to_string()` here?
> + })
> + .map(|(_id, route_map_entry)| {
> + let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
> + route_map.clone().into()
> + }))
> + }
> +
> + /// Update a RouteMap entry.
> + #[export]
> + pub fn update(
> + #[try_from_ref] this: &PerlRouteMapConfig,
> + route_map_id: RouteMapId,
> + order: u32,
> + updater: RouteMapEntryUpdater,
> + delete: Option<Vec<RouteMapDeletableProperties>>,
> + ) -> Result<(), Error> {
> + if updater.is_empty() && delete.is_empty() {
> + return Ok(());
> + }
> +
> + let mut route_maps = this.route_maps.lock().unwrap();
> + let id = RouteMapEntryId::new(route_map_id, order).to_string();
> +
> + let ConfigRouteMap::RouteMapEntry(route_map) = route_maps
> + .get_mut(&id)
> + .ok_or_else(|| anyhow!("Could not find route map with id: {}", id))?;
> +
> + let RouteMapEntryUpdater {
> + action,
> + set_actions,
> + match_actions,
> + } = updater;
> +
> + if let Some(action) = action {
> + route_map.set_action(action);
> + }
> +
> + if let Some(match_actions) = match_actions {
> + route_map.set_match_actions(match_actions);
> + }
> +
> + if let Some(set_actions) = set_actions {
> + route_map.set_set_actions(set_actions);
> + }
> +
> + for deletable_property in delete.unwrap_or_default() {
> + match deletable_property {
> + RouteMapDeletableProperties::SetActions => {
> + route_map.set_set_actions(Vec::new());
> + }
> + RouteMapDeletableProperties::MatchActions => {
> + route_map.set_match_actions(Vec::new());
> + }
> + }
> + }
> +
> + Ok(())
> + }
> +
> + /// Delete an entry in a RouteMap.
> + #[export]
> + pub fn delete(
> + #[try_from_ref] this: &PerlRouteMapConfig,
> + route_map_id: RouteMapId,
> + order: u32,
> + ) -> Result<(), Error> {
> + let id = RouteMapEntryId::new(route_map_id, order).to_string();
> +
> + this.route_maps
> + .lock()
> + .unwrap()
> + .remove(&id.to_string())
> + .ok_or_else(|| anyhow!("could not find route map entry with id: {id}"))?;
> +
> + Ok(())
> + }
> +}
> --
> 2.47.3
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 3/9] frr: allow rendering prefix-lists/route-maps separately
2026-03-25 14:32 ` Gabriel Goller
@ 2026-03-26 12:17 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-26 12:17 UTC (permalink / raw)
To: pve-devel
On 3/25/26 3:31 PM, Gabriel Goller wrote:
> On 25.03.2026 10:41, Stefan Hanreich wrote:
>> This is mainly useful for integration tests for the route maps /
>> prefix lists feature in ve-config. It utilizes those methods to render
>> route maps and prefix lists in tests, without having to render a full
>> FRR configuration.
>
> Why can't we move the inline tests into a tests/ dir (maybe in ve-config for now)
> and just generate the whole config (using Default we should be able to just omit
> the rest of the config)? IMO this is a bit weird having these two test-helpers
> here. This also doesn't catch errors importing the jinja template.
The idea was that this is less prone to changes outside of the
prefix_list / access_list template (e.g. FRR version bumps) and doesn't
require us to include the sections before / after the FRR configuration
in the assertions. I guess moving this into its own integration test and
using Default::default is better...
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 7/9] ve-config: add route map section config
2026-03-25 14:35 ` Gabriel Goller
@ 2026-03-26 13:49 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-26 13:49 UTC (permalink / raw)
To: pve-devel
ah, forgot to remove this again during development - fixed!
3/25/26 3:35 PM, Gabriel Goller wrote:
> On 25.03.2026 10:41, Stefan Hanreich wrote:
>> Those types represent FRR route maps inside a section config format.
>> For an example of the exact format and its FRR representation see the
>> module-level documentation.
>>
>> One section config entry maps to one route map entry. A route map
>> consists of one or more route map entries inside the section config.
>> The ID of a section encodes the name of the route map as well as the
>> order # of the entry.
>>
>> The route map module exports specific types for the API that handle
>> converting the section config ID, because currently it is only
>> possible to deserialize section config IDs to Strings. To avoid
>> having to implement the parsing logic along every step of the stack
>> (Perl backend, UI), use specific API types in the public API that
>> handle parsing the section ID into route map name and order.
>>
>> Contrary to most SDN entities, route maps IDs can be 32 characters
>> long instead of 8 and support underscores as well as hyphens. This is
>> because the restriction of having to generate network interface names
>> does not apply to FRR entities, so we can be more lenient with IDs
>> here, allowing users to specify more descriptive names.
>>
>> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
>> ---
>> proxmox-ve-config/debian/control | 2 +
>> proxmox-ve-config/src/sdn/mod.rs | 1 +
>> proxmox-ve-config/src/sdn/route_map.rs | 491 +++++++++++++++++++++++++
>> 3 files changed, 494 insertions(+)
>> create mode 100644 proxmox-ve-config/src/sdn/route_map.rs
>>
>> diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
>> index 440cf73..5206340 100644
>> --- a/proxmox-ve-config/debian/control
>> +++ b/proxmox-ve-config/debian/control
>> @@ -24,6 +24,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
>> librust-serde-1+default-dev <!nocheck>,
>> librust-serde-1+derive-dev <!nocheck>,
>> librust-serde-json-1+default-dev <!nocheck>,
>> + librust-serde-with-3+default-dev <!nocheck>,
>> librust-thiserror-2+default-dev <!nocheck>,
>> librust-tracing-0.1+default-dev (>= 0.1.37-~~) <!nocheck>
>> Maintainer: Proxmox Support Team <support@proxmox.com>
>> @@ -55,6 +56,7 @@ Depends:
>> librust-serde-1+default-dev,
>> librust-serde-1+derive-dev,
>> librust-serde-json-1+default-dev,
>> + librust-serde-with-3+default-dev,
>> librust-thiserror-2+default-dev,
>> librust-tracing-0.1+default-dev (>= 0.1.37-~~)
>> Suggests:
>
> serde-with is not import and used AFAICS.
>
>> diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
>> index 344c02c..24069ad 100644
>> --- a/proxmox-ve-config/src/sdn/mod.rs
>> +++ b/proxmox-ve-config/src/sdn/mod.rs
>> @@ -2,6 +2,7 @@ pub mod config;
>> pub mod fabric;
>> pub mod ipam;
>> pub mod prefix_list;
>> +pub mod route_map;
>>
>> use std::{error::Error, fmt::Display, str::FromStr};
>>
>> diff --git a/proxmox-ve-config/src/sdn/route_map.rs b/proxmox-ve-config/src/sdn/route_map.rs
>> new file mode 100644
>> index 0000000..3f4da56
>> --- /dev/null
>> +++ b/proxmox-ve-config/src/sdn/route_map.rs
>> @@ -0,0 +1,491 @@
>> +//! Section config types for FRR Route Maps.
>> +//!
>> +//! This module contains the API types required for representing FRR Route Maps as section config.
>> +//! Each entry in the section config maps to a Route Map entry, *not* a route map as a whole, the
>> +//! order of the entry is encoded in the ID of the Route Map.
>> +//!
>> +//! Route maps in FRR consists of at least one entry, which are ordered by their given sequence
>> +//! number / order. Each entry has a default matching policy, which is applied if the matching
>> +//! conditions of the entry are met.
>> +//!
>> +//! An example for a simple FRR Route Map entry loooks like this:
>> +//!
>> +//! ```text
>> +//! route-map test permit 10
>> +//! match ip next-hop address 192.0.2.1
>> +//! set local-preference 200
>> +//! ```
>> +//!
>> +//! The corresponding representation as a section config entry looks like this:
>> +//!
>> +//! ```text
>> +//! route-map-entry: test_10
>> +//! action permit
>> +//! match key=ip-next-hop-address,value=192.0.2.1
>> +//! set key=local-preference,value=200
>> +//! ```
>> +//!
>> +//! Match and Set Actions are encoded as an array with a property string that has a key and an
>> +//! optional value paramter, because some options do not require an additional value.
>> +//!
>> +//! This abstraction currently supports Match and Set actions, but not call actions and exit
>> +//! actions.
>> +
>> +use core::net::IpAddr;
>
> We usually always use imports from std:: (also in libraries). Note that this
> series introduces core:: imports in a few ve-rs patches.
>
>> +
>> +use anyhow::format_err;
>> +use const_format::concatcp;
>> +
>> +use proxmox_network_types::ip_address::api_types::{Ipv4Addr, Ipv6Addr};
>> +use proxmox_sdn_types::{
>> + bgp::{EvpnRouteType, SetMetricValue, SetTagValue},
>> + IntegerWithSign, Vni,
>> [snip]
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 8/9] ve-config: frr: implement frr config generation for route maps
2026-03-25 15:03 ` Gabriel Goller
@ 2026-03-26 13:50 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-26 13:50 UTC (permalink / raw)
To: pve-devel
On 3/25/26 4:02 PM, Gabriel Goller wrote:
> On 25.03.2026 10:41, Stefan Hanreich wrote:
>> Implements conversion traits for all the section config types, so they
>> can be converted into their respective FRR template counterpart.
>>
>> This module contains a helper for adding all route map entries to an
>> existing FRR configuration. It will overwrite existing route map
>> entries that have the same name AND order number. But if entries with
>> the same name, but different ordering, exist they will only be added
>> to the existing FRR configuration without dropping the other route map
>> entries.
>> This currently not relevant either way, because the initial API
>> implementation will reject creating route maps with names of route
>> maps that the stack auto-generates. In the future this behavior can
>> be used for selectively overriding / appending existing Proxmox VE
>> route maps.
>>
>> The helper also automatically orders route map entries according to
>> their ordering number. This allows for deterministic FRR configuration
>> output, which is important for tests and convenient for human
>> readability.
>>
>> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
>> ---
>> proxmox-ve-config/src/sdn/route_map.rs | 271 +++++++++++++++++++++++++
>> 1 file changed, 271 insertions(+)
>>
>> diff --git a/proxmox-ve-config/src/sdn/route_map.rs b/proxmox-ve-config/src/sdn/route_map.rs
>> index 3f4da56..8d8c4dc 100644
>> --- a/proxmox-ve-config/src/sdn/route_map.rs
>> +++ b/proxmox-ve-config/src/sdn/route_map.rs
> [snip]
>> + impl Into<RouteMapMatch> for MatchAction {
>> + fn into(self) -> RouteMapMatch {
>> + match self {
>> + Self::RouteType(evpn_route_type) => RouteMapMatch::RouteType(evpn_route_type),
>> + Self::Vni(vni) => RouteMapMatch::Vni(vni),
>> + Self::IpAddressPrefixList(prefix_list_name) => {
>> + RouteMapMatch::IpAddressPrefixList(prefix_list_name.into())
>> + }
>> + Self::Ip6AddressPrefixList(prefix_list_name) => {
>> + RouteMapMatch::Ip6AddressPrefixList(prefix_list_name.into())
>> + }
>> + Self::IpNextHopPrefixList(prefix_list_name) => {
>> + RouteMapMatch::IpNextHopPrefixList(prefix_list_name.into())
>> + }
>> + Self::Ip6NextHopPrefixList(prefix_list_name) => {
>> + RouteMapMatch::Ip6NextHopPrefixList(prefix_list_name.into())
>> + }
>> + Self::IpNextHopAddress(ipv4_addr) => RouteMapMatch::IpNextHopAddress(*ipv4_addr),
>> + Self::Ip6NextHopAddress(ipv6_addr) => RouteMapMatch::Ip6NextHopAddress(*ipv6_addr),
>> + Self::Metric(metric) => RouteMapMatch::Metric(metric),
>> + Self::LocalPreference(local_preference) => {
>> + RouteMapMatch::LocalPreference(local_preference)
>> + }
>> + Self::Peer(ip_addr) => RouteMapMatch::Peer(ip_addr),
>> + Self::Tag(tag) => RouteMapMatch::Tag(tag),
>> + }
>> + }
>> + }
>> +
>> + impl Into<RouteMapSet> for SetAction {
>> + fn into(self) -> RouteMapSet {
>> + match self {
>> + Self::IpNextHopPeerAddress => RouteMapSet::IpNextHopPeerAddress,
>> + Self::IpNextHopUnchanged => RouteMapSet::IpNextHopUnchanged,
>> + Self::IpNextHop(ipv4_addr) => RouteMapSet::IpNextHop(*ipv4_addr),
>> + Self::Ip6NextHopPeerAddress => RouteMapSet::Ip6NextHopPeerAddress,
>> + Self::Ip6NextHopPreferGlobal => RouteMapSet::Ip6NextHopPreferGlobal,
>> + Self::Ip6NextHop(ipv6_addr) => RouteMapSet::Ip6NextHop(*ipv6_addr),
>> + Self::LocalPreference(local_preference) => {
>> + RouteMapSet::LocalPreference(local_preference)
>> + }
>> + Self::Tag(tag) => RouteMapSet::Tag(tag),
>> + Self::Weight(weight) => RouteMapSet::Weight(weight),
>> + Self::Metric(metric) => RouteMapSet::Metric(metric),
>> + Self::Src(src) => RouteMapSet::Src(src),
>> + }
>> + }
>> + }
>> +
>> + impl Into<FrrRouteMap> for RouteMapEntry {
>> + fn into(self) -> FrrRouteMap {
>> + FrrRouteMap {
>> + seq: self.id.order,
>> + action: match self.action {
>> + RouteMapAction::Permit => proxmox_frr::ser::route_map::AccessAction::Permit,
>> + RouteMapAction::Deny => proxmox_frr::ser::route_map::AccessAction::Deny,
>> + },
>> + matches: self
>> + .match_actions
>> + .into_iter()
>> + .map(|match_action| match_action.into_inner().into())
>> + .collect(),
>> + sets: self
>> + .set_actions
>> + .into_iter()
>> + .map(|set_action| set_action.into_inner().into())
>> + .collect(),
>> + custom_frr_config: Default::default(),
>> + }
>> + }
>> + }
>
> These impls above could be From<>, even though we never need the other direction
> :) clippy otherwise complains.
fixed this - thanks!
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-perl-rs 1/3] pve-rs: sdn: add route maps module
2026-03-26 10:32 ` Wolfgang Bumiller
@ 2026-03-26 13:57 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-26 13:57 UTC (permalink / raw)
To: Wolfgang Bumiller; +Cc: pve-devel
On 3/26/26 11:31 AM, Wolfgang Bumiller wrote:
[snip]
>> +
>> + /// Returns a list of all RouteMap entries.
>
> ^ Method:
fixed this, and all other occurences, in the prefix list module as well
>> + #[export]
>> + pub fn list(
>> + #[try_from_ref] this: &PerlRouteMapConfig,
>> + ) -> Result<HashMap<String, ApiRouteMap>, Error> {
>> + Ok(this
>> + .route_maps
>> + .lock()
>> + .unwrap()
>> + .iter()
>> + .map(|(id, route_map_entry)| {
>> + let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
>> + (id.clone(), route_map.clone().into())
>> + })
>> + .collect())
>> + }
>> +
>> + /// Returns a list of all RouteMap entries for a given RouteMap ID.
>
> ^ Method:
>
> and it doesn't return a list - which in perl is quite specific.
> (Note that perlmod gained support for returning a *list* via
> `perlmod::ser::Return`'s `List(T)` variant, as well as a `@`-like final
> list parameter via the `#[list]` attribute. Also note that the latter is
> not meant to be used for anything other than implementing a pre-existing
> *perl* API)
yeah that makes sense as well. this only refers to the comment though,
not the method name afaict?
might make sense to potentially rename the method as well?
fixed the comment for now, in the prefix list module as well
>> + #[export]
>> + pub fn create(
>> + #[try_from_ref] this: &PerlRouteMapConfig,
>> + route_map: ApiRouteMap,
>> + ) -> Result<(), Error> {
>> + let mut route_maps = this.route_maps.lock().unwrap();
>> +
>> + let id =
>> + RouteMapEntryId::new(route_map.route_map_id().clone(), route_map.order()).to_string();
>
> So the key we use in `route_maps` is constructed from the route_map's id
> and order...
>
>> + let config_route_map = ConfigRouteMap::RouteMapEntry(route_map.into());
>> +
>> + if route_maps.get(&id).is_some() {
>> + anyhow::bail!("route map entry already exists in configuration: {}", id);
>> + }
>> +
>> + route_maps.insert(id, config_route_map);
>
> ^ The above two should probably use the entry api
>
> route_maps.entry() {
> Entry::Occupied(_) => bail!(...),
> Entry::Vacant(vacancy) => vacancy.insert(...),
> }
fixed here and in the prefix list module!
>> +
>> + Ok(())
>> + }
>> +
>> + /// Returns a specfic entry of a RouteMap.
>> + #[export]
>> + pub fn get(
>> + #[try_from_ref] this: &PerlRouteMapConfig,
>> + route_map_id: RouteMapId,
>> + order: u32,
>> + ) -> Result<Option<ApiRouteMap>, Error> {
>> + let id = RouteMapEntryId::new(route_map_id, order);
>> +
>> + Ok(this
>> + .route_maps
>> + .lock()
>> + .unwrap()
>> + .iter()
>> + .find(|(_id, route_map_entry)| {
>> + let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
>> + route_map.id() == &id
>
> ...so could we just `.get()` with a `RouteMapEntryId::new(route_map_id,
> order).to_string()` here?
yeah, idk what happened there - fixed it here and in the prefix list module!
[snip]
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 2/9] frr: implement routemap match/set statements via adjacent tagging
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 2/9] frr: implement routemap match/set statements via adjacent tagging Stefan Hanreich
@ 2026-03-26 14:44 ` Hannes Laimer
2026-03-27 9:02 ` Stefan Hanreich
0 siblings, 1 reply; 62+ messages in thread
From: Hannes Laimer @ 2026-03-26 14:44 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
On 2026-03-25 10:41, Stefan Hanreich wrote:
> Previously the types used a mix of adjacent / internal tagging and a
> nesting of types to represent match and set statements. This has been
> simplified by utilizing adjacent tagging on the set / match statements
> and using the exact FRR configuration key as the tag. This way a
> single enum can be used to represent match / set statements and all
> variants can be rendered the same by simply printing the keys /
> values.
>
> This commit also adds a lot of new match / set statements that were
> previously not supported. The crate supports now almost all match /
> set statements that FRR supports - with only a few having been
> omitted. Most notably it is not possible to match on community lists,
> support for those is planned in a future patch series.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
[..]
> diff --git a/proxmox-frr/src/ser/route_map.rs b/proxmox-frr/src/ser/route_map.rs
> index d12ae05..22807f1 100644
> --- a/proxmox-frr/src/ser/route_map.rs
> +++ b/proxmox-frr/src/ser/route_map.rs
> @@ -1,6 +1,11 @@
> +use core::net::{Ipv4Addr, Ipv6Addr};
> use std::net::IpAddr;
>
> use proxmox_network_types::ip_address::Cidr;
> +use proxmox_sdn_types::{
> + bgp::{EvpnRouteType, SetMetricValue, SetTagValue},
> + IntegerWithSign, Vni,
> +};
> use serde::{Deserialize, Serialize};
>
> /// The action for a [`AccessListRule`].
> @@ -45,6 +50,12 @@ impl AccessListName {
> }
> }
>
> +impl PrefixListName {
> + pub fn new(name: String) -> PrefixListName {
> + PrefixListName(name)
> + }
> +}
the same `impl` block already exists 10 lines above this one :)
[..]
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 03/13] api2: add prefix list module
2026-03-25 9:41 ` [PATCH pve-network 03/13] api2: add prefix list module Stefan Hanreich
@ 2026-03-26 15:01 ` Hannes Laimer
2026-03-27 9:57 ` Stefan Hanreich
0 siblings, 1 reply; 62+ messages in thread
From: Hannes Laimer @ 2026-03-26 15:01 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
On 2026-03-25 10:43, Stefan Hanreich wrote:
> Contains the CRUD functionality for prefix lists:
>
> GET /prefix-lists - lists all prefix lists
> GET /prefix-lists/<id> - get prefix list <id>
> POST /prefix-lists - create a new prefix list
> PUT /prefix-lists/<id> - update prefix list <id>
> DELETE /prefix-lists/<id> - delete prefix list <id>
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
[..]
> +__PACKAGE__->register_method({
> + name => 'list_prefix_lists',
> + path => '',
> + method => 'GET',
> + permissions => {
> + description =>
> + "Only returns prefix list entries where you have 'Sys.Audit' or 'Sys.Modify' permissions.",
> + },
> + description => "List Prefix Lists",
[..]
> + code => sub {
> + my ($param) = @_;
> +
> + my $pending = extract_param($param, 'pending');
> + my $running = extract_param($param, 'running');
> +
> + my $digest;
> + my $prefix_lists;
> +
> + if ($pending) {
> + my $current_config = PVE::Network::SDN::PrefixLists::config()->list();
> + my $running_config = PVE::Network::SDN::PrefixLists::config(1)->list();
> +
> + my $pending_prefix_lists = PVE::Network::SDN::pending_config(
> + $running_config,
> + $current_config,
```
my $running_objects = $running_cfg->{$type}->{ids};
my $config_objects = $cfg->{ids};
```
is how `pending_config(..)` accesses these two, since `->list()` just
returns a hash we have to wrap these two
```
- $running_config,
- $current_config,
+ { 'route-maps' => { ids => $running_route_maps } },
+ { ids => $current_route_maps },
```
> + 'prefix-lists',
> + );
> +
> + $digest = $current_config->digest();
> + $prefix_lists = $pending_prefix_lists->{ids}
> + } elsif ($running) {
[..]
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 05/13] api2: add route maps api module
2026-03-25 9:41 ` [PATCH pve-network 05/13] api2: add route maps api module Stefan Hanreich
@ 2026-03-26 15:05 ` Hannes Laimer
2026-03-27 9:57 ` Stefan Hanreich
0 siblings, 1 reply; 62+ messages in thread
From: Hannes Laimer @ 2026-03-26 15:05 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
On 2026-03-25 10:44, Stefan Hanreich wrote:
> Contains the following API endpoints:
>
> GET /route-maps - lists all route map entries
> POST /route-maps - creates a new route map entry
>
> The following commits contain the API modules for accessing specific
> route maps / route map entries.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
[..]
> +__PACKAGE__->register_method({
> + name => 'list_route_maps',
> + path => '',
> + method => 'GET',
> + permissions => {
> + description =>
> + "Only returns route map entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions.",
> + },
> + description => "List Route Maps",
[..]
> + code => sub {
> + my ($param) = @_;
> +
> + my $pending = extract_param($param, 'pending');
> + my $running = extract_param($param, 'running');
> +
> + my $digest;
> + my $route_maps;
> +
> + if ($pending) {
> + my $current_config = PVE::Network::SDN::RouteMaps::config()->list();
> + my $running_config = PVE::Network::SDN::RouteMaps::config(1)->list();
> +
> + my $pending_route_maps = PVE::Network::SDN::pending_config(
> + $running_config,
> + $current_config,
we should wrap these two, as on pve-network 3/13
> + 'route-maps',
> + );
> +
> + $digest = $current_config->digest();
> + $route_maps = $pending_route_maps->{ids}
> + } elsif ($running) {
[..]
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 06/13] api2: add route map module
2026-03-25 9:41 ` [PATCH pve-network 06/13] api2: add route map module Stefan Hanreich
@ 2026-03-26 15:07 ` Hannes Laimer
2026-03-27 9:57 ` Stefan Hanreich
0 siblings, 1 reply; 62+ messages in thread
From: Hannes Laimer @ 2026-03-26 15:07 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
On 2026-03-25 10:44, Stefan Hanreich wrote:
> This module contains the following API endpoint:
>
> GET /route-maps/<id> - lists all route map entries for the route map
> <id>
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
[..]
> +__PACKAGE__->register_method({
> + name => 'list_route_map_entries',
> + path => '',
> + method => 'GET',
> + permissions => {
> + check => ['perm', '/sdn/route-maps/{route_map_id}', ['SDN.Audit']],
> + },
> + description => "List all entries for a given Route Map",
[..]
> + code => sub {
> + my ($param) = @_;
> +
> + my $pending = extract_param($param, 'pending');
> + my $running = extract_param($param, 'running');
> + my $route_map_id = extract_param($param, 'route-map-id');
> +
> + my $digest;
> + my $route_map_entries;
> +
> + if ($pending) {
> + my $current_config = PVE::Network::SDN::RouteMaps::config()->list_route_map($route_map_id);
> + my $running_config = PVE::Network::SDN::RouteMaps::config(1)->list_route_map($route_map_id);
> +
> + my $pending_route_maps = PVE::Network::SDN::pending_config(
> + $running_config,
> + $current_config,
we should wrap these two, like pve-network 03/13 :P
> + 'route-maps',
> + );
> +
> + $digest = $current_config->digest();
> + $route_map_entries = $pending_route_maps->{ids}
> + } elsif ($running) {
[..]
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 07/13] api2: add route map entry module
2026-03-25 9:41 ` [PATCH pve-network 07/13] api2: add route map entry module Stefan Hanreich
@ 2026-03-26 15:13 ` Hannes Laimer
2026-03-27 10:01 ` Stefan Hanreich
0 siblings, 1 reply; 62+ messages in thread
From: Hannes Laimer @ 2026-03-26 15:13 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
On 2026-03-25 10:49, Stefan Hanreich wrote:
> This module contains the following API endpoints:
>
> GET /route-maps/<id>/<order> - gets the order'th entry in route map
> <id>
> PUT /route-maps/<id>/<order> - updates the order'th entry in
> route map <id>
> DELETE /route-maps/<id>/<order> - deletes the order'th
> entry in route map <id>
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
[..]
> +__PACKAGE__->register_method({
> + name => 'update_route_map_entry',
> + path => '',
> + method => 'PUT',
> + permissions => {
> + check => ['perm', '/sdn/route-maps/{route-map-id}', ['SDN.Allocate']],
> + },
> + description => "Update Route Map Entry",
> + parameters => {
> + properties => {
> + digest => get_standard_option('pve-config-digest'),
> + 'lock-token' => get_standard_option('pve-sdn-lock-token'),
> + PVE::Network::SDN::RouteMaps::route_map_properties(1)->%*,
> + },
> + },
> + returns => {
> + type => "null",
> + },
> + code => sub {
> + my ($param) = @_;
> +
> + my $lock_token = extract_param($param, 'lock-token');
> +
> + PVE::Network::SDN::lock_sdn_config(
> + sub {
> + my $config = PVE::Network::SDN::RouteMaps::config();
> +
> + my $digest = extract_param($param, 'digest');
> + PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
> +
> + my $route_map_id = extract_param($param, 'route-map-id');
> + my $order = extract_param($param, 'order');
> + my $delete = extract_param($param, 'delete');
> +
> + $config->update($route_map_id, $order, $param);
looks like the update call is missing the `.. , $delete);`
> + PVE::Network::SDN::RouteMaps::write_config($config);
> + },
> + "updating route map entry failed",
> + $lock_token,
> + );
> +
> + return;
> + },
> +});
[..]
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 01/13] controller: bgp: evpn: adapt to new match / set frr config syntax
2026-03-25 9:41 ` [PATCH pve-network 01/13] controller: bgp: evpn: adapt to new match / set frr config syntax Stefan Hanreich
@ 2026-03-26 15:19 ` Hannes Laimer
2026-03-27 10:05 ` Stefan Hanreich
0 siblings, 1 reply; 62+ messages in thread
From: Hannes Laimer @ 2026-03-26 15:19 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
On 2026-03-25 10:41, Stefan Hanreich wrote:
> proxmox-frr has changed the representation of match / set statements
> in its structs. It has changed to generic key / value fields, where
> the keys specify the literal name of the key in the FRR configuration.
>
> Adapt the controllers to use the new fields so the configuration gets
> generated properly.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
[..]
> diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
> index 8891541..d54c9ec 100644
> --- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
> +++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
> @@ -136,13 +136,12 @@ sub generate_frr_config {
> $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' },
> + key => 'ip address prefix-list',
> + value => 'loopbacks_ips',
given that
```
+ if ($id =~ m/^(only_default|only_default_v6|loopback_ips|)$/) {
+ return undef if $noerr;
+ die "prefix list ID '$id' is currently reserved and cannot
be used\n";
+ }
```
is added in the next patch(02/13 pve-network), it looks like a `s`
slipped onto the end of `loopback` here :)
> };
> my $routemap = {
> matches => [$routemap_config],
> - sets => [{ set_type => 'src', value => $ifaceip }],
> + sets => [{ key => 'src', value => $ifaceip }],
> action => "permit",
> seq => 1,
> };
[...]
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 2/9] frr: implement routemap match/set statements via adjacent tagging
2026-03-26 14:44 ` Hannes Laimer
@ 2026-03-27 9:02 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 9:02 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
On 3/26/26 3:43 PM, Hannes Laimer wrote:
> On 2026-03-25 10:41, Stefan Hanreich wrote:
>> Previously the types used a mix of adjacent / internal tagging and a
>> nesting of types to represent match and set statements. This has been
>> simplified by utilizing adjacent tagging on the set / match statements
>> and using the exact FRR configuration key as the tag. This way a
>> single enum can be used to represent match / set statements and all
>> variants can be rendered the same by simply printing the keys /
>> values.
>>
>> This commit also adds a lot of new match / set statements that were
>> previously not supported. The crate supports now almost all match /
>> set statements that FRR supports - with only a few having been
>> omitted. Most notably it is not possible to match on community lists,
>> support for those is planned in a future patch series.
>>
>> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
>> ---
>
> [..]
>
>> diff --git a/proxmox-frr/src/ser/route_map.rs b/proxmox-frr/src/ser/route_map.rs
>> index d12ae05..22807f1 100644
>> --- a/proxmox-frr/src/ser/route_map.rs
>> +++ b/proxmox-frr/src/ser/route_map.rs
>> @@ -1,6 +1,11 @@
>> +use core::net::{Ipv4Addr, Ipv6Addr};
>> use std::net::IpAddr;
>>
>> use proxmox_network_types::ip_address::Cidr;
>> +use proxmox_sdn_types::{
>> + bgp::{EvpnRouteType, SetMetricValue, SetTagValue},
>> + IntegerWithSign, Vni,
>> +};
>> use serde::{Deserialize, Serialize};
>>
>> /// The action for a [`AccessListRule`].
>> @@ -45,6 +50,12 @@ impl AccessListName {
>> }
>> }
>>
>> +impl PrefixListName {
>> + pub fn new(name: String) -> PrefixListName {
>> + PrefixListName(name)
>> + }
>> +}
>
> the same `impl` block already exists 10 lines above this one :)
fixed! messed up when rebasing on top of Gabriel's series...
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 03/13] api2: add prefix list module
2026-03-26 15:01 ` Hannes Laimer
@ 2026-03-27 9:57 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 9:57 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
On 3/26/26 4:00 PM, Hannes Laimer wrote:
> On 2026-03-25 10:43, Stefan Hanreich wrote:
>> Contains the CRUD functionality for prefix lists:
>>
>> GET /prefix-lists - lists all prefix lists
>> GET /prefix-lists/<id> - get prefix list <id>
>> POST /prefix-lists - create a new prefix list
>> PUT /prefix-lists/<id> - update prefix list <id>
>> DELETE /prefix-lists/<id> - delete prefix list <id>
>>
>> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
>> ---
>
> [..]
>
>> +__PACKAGE__->register_method({
>> + name => 'list_prefix_lists',
>> + path => '',
>> + method => 'GET',
>> + permissions => {
>> + description =>
>> + "Only returns prefix list entries where you have 'Sys.Audit' or 'Sys.Modify' permissions.",
>> + },
>> + description => "List Prefix Lists",
>
> [..]
>
>> + code => sub {
>> + my ($param) = @_;
>> +
>> + my $pending = extract_param($param, 'pending');
>> + my $running = extract_param($param, 'running');
>> +
>> + my $digest;
>> + my $prefix_lists;
>> +
>> + if ($pending) {
>> + my $current_config = PVE::Network::SDN::PrefixLists::config()->list();
>> + my $running_config = PVE::Network::SDN::PrefixLists::config(1)->list();
>> +
>> + my $pending_prefix_lists = PVE::Network::SDN::pending_config(
>> + $running_config,
>> + $current_config,
>
>
> ```
> my $running_objects = $running_cfg->{$type}->{ids};
> my $config_objects = $cfg->{ids};
> ```
> is how `pending_config(..)` accesses these two, since `->list()` just
> returns a hash we have to wrap these two
> ```
> - $running_config,
> - $current_config,
> + { 'route-maps' => { ids => $running_route_maps } },
> + { ids => $current_route_maps },
> ```
good catch, there was an issue with calling list() as well, since we
cannot get the digest later on that I fixed as well, while I touched it.
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 05/13] api2: add route maps api module
2026-03-26 15:05 ` Hannes Laimer
@ 2026-03-27 9:57 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 9:57 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
On 3/26/26 4:04 PM, Hannes Laimer wrote:
> On 2026-03-25 10:44, Stefan Hanreich wrote:
>> Contains the following API endpoints:
>>
>> GET /route-maps - lists all route map entries
>> POST /route-maps - creates a new route map entry
>>
>> The following commits contain the API modules for accessing specific
>> route maps / route map entries.
>>
>> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
>> ---
>
> [..]
>
>> +__PACKAGE__->register_method({
>> + name => 'list_route_maps',
>> + path => '',
>> + method => 'GET',
>> + permissions => {
>> + description =>
>> + "Only returns route map entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions.",
>> + },
>> + description => "List Route Maps",
>
> [..]
>
>> + code => sub {
>> + my ($param) = @_;
>> +
>> + my $pending = extract_param($param, 'pending');
>> + my $running = extract_param($param, 'running');
>> +
>> + my $digest;
>> + my $route_maps;
>> +
>> + if ($pending) {
>> + my $current_config = PVE::Network::SDN::RouteMaps::config()->list();
>> + my $running_config = PVE::Network::SDN::RouteMaps::config(1)->list();
>> +
>> + my $pending_route_maps = PVE::Network::SDN::pending_config(
>> + $running_config,
>> + $current_config,
>
> we should wrap these two, as on pve-network 3/13
fixed in the same vein as 3/13
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 06/13] api2: add route map module
2026-03-26 15:07 ` Hannes Laimer
@ 2026-03-27 9:57 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 9:57 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
On 3/26/26 4:06 PM, Hannes Laimer wrote:
> On 2026-03-25 10:44, Stefan Hanreich wrote:
>> This module contains the following API endpoint:
>>
>> GET /route-maps/<id> - lists all route map entries for the route map
>> <id>
>>
>> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
>> ---
>
> [..]
>
>> +__PACKAGE__->register_method({
>> + name => 'list_route_map_entries',
>> + path => '',
>> + method => 'GET',
>> + permissions => {
>> + check => ['perm', '/sdn/route-maps/{route_map_id}', ['SDN.Audit']],
>> + },
>> + description => "List all entries for a given Route Map",
>
> [..]
>
>> + code => sub {
>> + my ($param) = @_;
>> +
>> + my $pending = extract_param($param, 'pending');
>> + my $running = extract_param($param, 'running');
>> + my $route_map_id = extract_param($param, 'route-map-id');
>> +
>> + my $digest;
>> + my $route_map_entries;
>> +
>> + if ($pending) {
>> + my $current_config = PVE::Network::SDN::RouteMaps::config()->list_route_map($route_map_id);
>> + my $running_config = PVE::Network::SDN::RouteMaps::config(1)->list_route_map($route_map_id);
>> +
>> + my $pending_route_maps = PVE::Network::SDN::pending_config(
>> + $running_config,
>> + $current_config,
>
> we should wrap these two, like pve-network 03/13 :P
fixed in the same vein as 3/13
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 07/13] api2: add route map entry module
2026-03-26 15:13 ` Hannes Laimer
@ 2026-03-27 10:01 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 10:01 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
On 3/26/26 4:12 PM, Hannes Laimer wrote:
> On 2026-03-25 10:49, Stefan Hanreich wrote:
>> This module contains the following API endpoints:
>>
>> GET /route-maps/<id>/<order> - gets the order'th entry in route map
>> <id>
>> PUT /route-maps/<id>/<order> - updates the order'th entry in
>> route map <id>
>> DELETE /route-maps/<id>/<order> - deletes the order'th
>> entry in route map <id>
>>
>> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
>> ---
>
> [..]
>
>> +__PACKAGE__->register_method({
>> + name => 'update_route_map_entry',
>> + path => '',
>> + method => 'PUT',
>> + permissions => {
>> + check => ['perm', '/sdn/route-maps/{route-map-id}', ['SDN.Allocate']],
>> + },
>> + description => "Update Route Map Entry",
>> + parameters => {
>> + properties => {
>> + digest => get_standard_option('pve-config-digest'),
>> + 'lock-token' => get_standard_option('pve-sdn-lock-token'),
>> + PVE::Network::SDN::RouteMaps::route_map_properties(1)->%*,
>> + },
>> + },
>> + returns => {
>> + type => "null",
>> + },
>> + code => sub {
>> + my ($param) = @_;
>> +
>> + my $lock_token = extract_param($param, 'lock-token');
>> +
>> + PVE::Network::SDN::lock_sdn_config(
>> + sub {
>> + my $config = PVE::Network::SDN::RouteMaps::config();
>> +
>> + my $digest = extract_param($param, 'digest');
>> + PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
>> +
>> + my $route_map_id = extract_param($param, 'route-map-id');
>> + my $order = extract_param($param, 'order');
>> + my $delete = extract_param($param, 'delete');
>> +
>> + $config->update($route_map_id, $order, $param);
>
> looks like the update call is missing the `.. , $delete);`
good catch, fixed
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 01/13] controller: bgp: evpn: adapt to new match / set frr config syntax
2026-03-26 15:19 ` Hannes Laimer
@ 2026-03-27 10:05 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 10:05 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
On 3/26/26 4:19 PM, Hannes Laimer wrote:
> On 2026-03-25 10:41, Stefan Hanreich wrote:
>> proxmox-frr has changed the representation of match / set statements
>> in its structs. It has changed to generic key / value fields, where
>> the keys specify the literal name of the key in the FRR configuration.
>>
>> Adapt the controllers to use the new fields so the configuration gets
>> generated properly.
>>
>> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
>> ---
>
> [..]
>
>> diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
>> index 8891541..d54c9ec 100644
>> --- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
>> +++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
>> @@ -136,13 +136,12 @@ sub generate_frr_config {
>> $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' },
>> + key => 'ip address prefix-list',
>> + value => 'loopbacks_ips',
>
> given that
> ```
> + if ($id =~ m/^(only_default|only_default_v6|loopback_ips|)$/) {
> + return undef if $noerr;
> + die "prefix list ID '$id' is currently reserved and cannot
> be used\n";
> + }
> ```
> is added in the next patch(02/13 pve-network), it looks like a `s`
> slipped onto the end of `loopback` here :)
it's actually the other way around - the pre-existing name is indeed
loopbacks_ips - so it needs to be fixed in the ID validation...
see
https://git.proxmox.com/?p=pve-network.git;a=blob;f=src/PVE/Network/SDN/Controllers/BgpPlugin.pm;h=447ebf1ba74492898b7692988e6d5fb230f55552;hb=HEAD#l128
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
` (27 preceding siblings ...)
2026-03-25 11:38 ` [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
@ 2026-03-27 10:17 ` Stefan Hanreich
28 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 10:17 UTC (permalink / raw)
To: pve-devel
Thanks for the reviews everyone, I'll wait on Gabriels v8 and send a
rebased version with all fixes shortly!
On 3/25/26 10:41 AM, Stefan Hanreich wrote:
> ## Introduction
>
> This patch adds support for managing route maps and prefix lists to the SDN
> stack. With this patch series, route maps can be applied to the BGP and EVPN
> controller for incoming / outgoing route filtering. There are currently some
> other features in development that would make use of route maps as well, namely:
>
> * VRF route leaking
> * Route Redistribution for Fabrics
>
> Prefix Lists can be used for matching inside route map match statements. They
> are implemented so they can be used inside route map match statements for now.
>
> ## Motivation
>
> There are a lot of use-cases for enabling users to create their own route-maps,
> which was currently only possible by utilizing frr.conf.local - which was clunky
> and prone to issues. Route maps can be used for filtering in/outgoing routes and
> modifiy them, so users could e.g. only selectively advertise some routes via BGP
> or only import certain EVPN routes from outside.
>
> It also allows us to programmatically manage route maps via the UI, e.g. for
> deeper EVPN integration in PDM. This opens up a lot of possibilities for
> new features.
>
>
> ## Configuration Format
>
> This patch series adds two new configuration files, route-maps.cfg and
> prefix-lists.cfg in /etc/pve/sdn.
>
> ### route-maps.cfg
>
> An example route map configuration looks as follows:
>
> route-map-entry: example_123
> action permit
> match key=vni,value=23487
> set key=tag,value=23487
>
> This would create the following FRR route map entry:
>
> route-map example permit 123
> match evpn vni 23487
> set tag 23487
>
> Every entry in route-maps.cfg maps to an entry in a route map. They are
> identified by their name as well as their ordering number. `example_123`
> specifies the 123th entry in the route map 'example'. The main reason for
> choosing this format is, that having a single section for one route-map would be
> quite unwieldy. It'd require some format like this, which is pretty awkward to
> handle / validate:
>
> route-map-entry: example
> action permit,seq=123
> match key=vni,value=23487,seq=123
> set key=tag,value=23487,seq=123
>
> From a UI POV editing singular route map entries seems better as well, and with
> the mapping of section entries to route map entries, a suitable API design
> follows quite naturally and easily maps to the respective section config
> entries, without too much data mangling required.
>
>
> ### prefix-lists.cfg
>
> An example prefix list configuration looks as follows:
>
> prefix-list: example-1
> entries action=permit,prefix=192.0.2.0/24
> entries action=permit,prefix=192.0.2.0/24,le=32
> entries action=permit,prefix=192.0.2.0/24,le=32,ge=24,seq=123
>
> This would create the following FRR prefix list:
>
> ip prefix-list example-1 permit 192.0.2.0/24
> ip prefix-list example-1 permit 192.0.2.0/24 le 32
> ip prefix-list example-1 seq 123 permit 192.0.2.0/24 le 32 ge 24
>
>
> ## API endpoints
>
> This patch series introduces the following API endpoints in the /cluster/sdn
> subfolder:
>
>
> ### Route Maps
>
> GET /route-maps - lists all route map entries
> GET /route-maps/<id> - lists all route map entries for the route map <id>
> GET /route-maps/<id>/<order> - gets the order'th entry in route map <id>
> POST /route-maps - creates a new route map entry
> PUT /route-maps/<id>/<order> - updates the order'th entry in route map <id>
> DELETE /route-maps/<id>/<order> - deletes the order'th entry in route map <id>
>
>
> ### Prefix Lists
>
> GET /prefix-lists - lists all prefix lists
> GET /prefix-lists/<id> - get prefix list <id>
> POST /prefix-lists - create a new prefix list
> PUT /prefix-lists/<id> - update prefix list <id>
> DELETE /prefix-lists/<id> - delete prefix list <id>
>
>
> ## Open questions
>
> How should we handle overriding the auto-generated route maps (e.g. in the EVPN
> controller) and prefix lists?
>
> Currently this patch series disallows creating any route map / prefix list that
> have the same name as PVE auto-generated ones via the API. They can be
> overridden by creating a new route map and then selecting it in the respective
> entity (e.g. via route-map-in in the EVPN controller). Pre-defined prefix-lists
> cannot currently be overridden, since this usually makes little sense, as they
> are used in the auto-generated route maps, which can be overriden anyway.
>
> pve-cluster:
>
> Stefan Hanreich (2):
> cfs: add 'sdn/route-maps.cfg' to observed files
> cfs: add 'sdn/prefix-lists.cfg' to observed files
>
> src/PVE/Cluster.pm | 2 ++
> src/pmxcfs/status.c | 2 ++
> 2 files changed, 4 insertions(+)
>
>
> proxmox-ve-rs:
>
> Stefan Hanreich (9):
> sdn-types: add common route-map helper types
> frr: implement routemap match/set statements via adjacent tagging
> frr: allow rendering prefix-lists/route-maps separately
> frr-templates: change route maps template to adapt to new types
> ve-config: add prefix list section config
> ve-config: frr: implement frr config generation for prefix lists
> ve-config: add route map section config
> ve-config: frr: implement frr config generation for route maps
> ve-config: fabrics: adapt frr config generation to new format
>
> .../templates/route_maps.jinja | 12 +-
> proxmox-frr/Cargo.toml | 2 +-
> proxmox-frr/src/ser/route_map.rs | 101 ++-
> proxmox-frr/src/ser/serializer.rs | 35 +-
> proxmox-sdn-types/src/bgp.rs | 50 ++
> proxmox-sdn-types/src/lib.rs | 135 ++++
> proxmox-ve-config/debian/control | 2 +
> proxmox-ve-config/src/sdn/fabric/frr.rs | 25 +-
> proxmox-ve-config/src/sdn/mod.rs | 2 +
> proxmox-ve-config/src/sdn/prefix_list.rs | 347 ++++++++
> proxmox-ve-config/src/sdn/route_map.rs | 762 ++++++++++++++++++
> 11 files changed, 1408 insertions(+), 65 deletions(-)
> create mode 100644 proxmox-sdn-types/src/bgp.rs
> create mode 100644 proxmox-ve-config/src/sdn/prefix_list.rs
> create mode 100644 proxmox-ve-config/src/sdn/route_map.rs
>
>
> proxmox-perl-rs:
>
> Stefan Hanreich (3):
> pve-rs: sdn: add route maps module
> pve-rs: sdn: add prefix lists module
> sdn: add prefix list / route maps to frr config generation helper
>
> pve-rs/Cargo.toml | 1 +
> pve-rs/Makefile | 2 +
> pve-rs/src/bindings/sdn/mod.rs | 30 ++-
> pve-rs/src/bindings/sdn/prefix_lists.rs | 199 +++++++++++++++++++
> pve-rs/src/bindings/sdn/route_maps.rs | 243 ++++++++++++++++++++++++
> 5 files changed, 472 insertions(+), 3 deletions(-)
> create mode 100644 pve-rs/src/bindings/sdn/prefix_lists.rs
> create mode 100644 pve-rs/src/bindings/sdn/route_maps.rs
>
>
> pve-network:
>
> Stefan Hanreich (13):
> controller: bgp: evpn: adapt to new match / set frr config syntax
> sdn: add prefix lists module
> api2: add prefix list module
> sdn: add route map module
> api2: add route maps api module
> api2: add route map module
> api2: add route map entry module
> evpn controller: add route_map_{in,out} parameter
> sdn: generate route map / prefix list configuration on sdn apply
> tests: add simple route map test case
> tests: add bgp evpn route map/prefix list testcase
> tests: add route map with prefix list testcase
> bgp controller: allow configuring custom route maps
>
> src/PVE/API2/Network/SDN.pm | 14 +
> src/PVE/API2/Network/SDN/Makefile | 13 +-
> src/PVE/API2/Network/SDN/PrefixLists.pm | 247 ++++++++++++++++++
> src/PVE/API2/Network/SDN/RouteMaps.pm | 137 ++++++++++
> src/PVE/API2/Network/SDN/RouteMaps/Makefile | 9 +
> .../API2/Network/SDN/RouteMaps/RouteMap.pm | 92 +++++++
> .../Network/SDN/RouteMaps/RouteMapEntry.pm | 136 ++++++++++
> src/PVE/Network/SDN.pm | 14 +-
> src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 22 +-
> src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 43 +--
> src/PVE/Network/SDN/Controllers/Plugin.pm | 14 +
> src/PVE/Network/SDN/Makefile | 14 +-
> src/PVE/Network/SDN/PrefixLists.pm | 134 ++++++++++
> src/PVE/Network/SDN/RouteMaps.pm | 173 ++++++++++++
> .../expected_controller_config | 76 ++++++
> .../expected_sdn_interfaces | 41 +++
> .../bgp_evpn_routemap_prefix_list/interfaces | 7 +
> .../bgp_evpn_routemap_prefix_list/sdn_config | 86 ++++++
> .../evpn/routemap/expected_controller_config | 60 +++++
> .../evpn/routemap/expected_sdn_interfaces | 41 +++
> src/test/zones/evpn/routemap/interfaces | 7 +
> src/test/zones/evpn/routemap/sdn_config | 70 +++++
> .../expected_controller_config | 49 ++++
> .../expected_sdn_interfaces | 41 +++
> .../evpn/routemap_prefix_list/interfaces | 7 +
> .../evpn/routemap_prefix_list/sdn_config | 58 ++++
> 26 files changed, 1572 insertions(+), 33 deletions(-)
> create mode 100644 src/PVE/API2/Network/SDN/PrefixLists.pm
> create mode 100644 src/PVE/API2/Network/SDN/RouteMaps.pm
> create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/Makefile
> create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
> create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
> create mode 100644 src/PVE/Network/SDN/PrefixLists.pm
> create mode 100644 src/PVE/Network/SDN/RouteMaps.pm
> create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_controller_config
> create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/expected_sdn_interfaces
> create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/interfaces
> create mode 100644 src/test/zones/evpn/bgp_evpn_routemap_prefix_list/sdn_config
> create mode 100644 src/test/zones/evpn/routemap/expected_controller_config
> create mode 100644 src/test/zones/evpn/routemap/expected_sdn_interfaces
> create mode 100644 src/test/zones/evpn/routemap/interfaces
> create mode 100644 src/test/zones/evpn/routemap/sdn_config
> create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_controller_config
> create mode 100644 src/test/zones/evpn/routemap_prefix_list/expected_sdn_interfaces
> create mode 100644 src/test/zones/evpn/routemap_prefix_list/interfaces
> create mode 100644 src/test/zones/evpn/routemap_prefix_list/sdn_config
>
>
> Summary over all repositories:
> 44 files changed, 3456 insertions(+), 101 deletions(-)
>
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 08/13] evpn controller: add route_map_{in,out} parameter
2026-03-25 9:41 ` [PATCH pve-network 08/13] evpn controller: add route_map_{in,out} parameter Stefan Hanreich
@ 2026-03-27 10:44 ` Hannes Laimer
2026-03-27 11:12 ` Stefan Hanreich
0 siblings, 1 reply; 62+ messages in thread
From: Hannes Laimer @ 2026-03-27 10:44 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
in `generate_zone_frr_config` we probably also want to use the newly
introduced `route-map-[in|out]` parameter, not the hard coded ones
On 2026-03-25 10:42, Stefan Hanreich wrote:
> This parameter allows overriding the default MAP_VTEP_{IN,OUT} route
> maps by specifying a custom route map configured in route-maps.cfg.
> This can be used for filtering incoming and outgoing routes, e.g. for
> only advertising type-5 routes to external peers or only allow
> importing routes with specific route targets.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 19 +++++++++++++------
> src/PVE/Network/SDN/Controllers/Plugin.pm | 14 ++++++++++++++
> 2 files changed, 27 insertions(+), 6 deletions(-)
>
> diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
> index 3e643b1..d7b838b 100644
> --- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
> +++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
> @@ -45,6 +45,8 @@ sub options {
> 'asn' => { optional => 0 },
> 'peers' => { optional => 1 },
> 'fabric' => { optional => 1 },
> + 'route-map-in' => { optional => 1 },
> + 'route-map-out' => { optional => 1 },
> };
> }
>
> @@ -153,23 +155,28 @@ sub generate_frr_config {
>
> push @{ $bgp_router->{neighbor_groups} }, $neighbor_group;
>
> + my $route_map_in = $plugin_config->{'route-map-in'} // 'MAP_VTEP_IN';
> + my $route_map_out = $plugin_config->{'route-map-out'} // 'MAP_VTEP_OUT';
> +
> # 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',
> + route_map_in => $route_map_in,
> + route_map_out => $route_map_out,
> }],
> advertise_all_vni => 1,
> };
>
> $bgp_router->{address_families}->{l2vpn_evpn}->{autort_as} = $autortas if $autortas;
>
> - my $routemap_in = { seq => 1, action => "permit" };
> - my $routemap_out = { seq => 1, action => "permit" };
> + if ($route_map_in eq 'MAP_VTEP_IN' && !$config->{frr}->{routemaps}->{'MAP_VTEP_IN'}) {
> + push($config->{frr}->{routemaps}->{'MAP_VTEP_IN'}->@*, { seq => 1, action => "permit" });
> + }
>
> - push($config->{frr}->{routemaps}->{'MAP_VTEP_IN'}->@*, $routemap_in);
> - push($config->{frr}->{routemaps}->{'MAP_VTEP_OUT'}->@*, $routemap_out);
> + if ($route_map_out eq 'MAP_VTEP_OUT' && !$config->{frr}->{routemaps}->{'MAP_VTEP_OUT'}) {
> + push($config->{frr}->{routemaps}->{'MAP_VTEP_OUT'}->@*, { seq => 1, action => "permit" });
> + }
>
> return $config;
> }
> diff --git a/src/PVE/Network/SDN/Controllers/Plugin.pm b/src/PVE/Network/SDN/Controllers/Plugin.pm
> index d70e518..5f9f1ef 100644
> --- a/src/PVE/Network/SDN/Controllers/Plugin.pm
> +++ b/src/PVE/Network/SDN/Controllers/Plugin.pm
> @@ -7,6 +7,8 @@ use PVE::Tools;
> use PVE::JSONSchema;
> use PVE::Cluster;
>
> +use PVE::Network::SDN::RouteMaps;
> +
> use PVE::JSONSchema qw(get_standard_option);
> use base qw(PVE::SectionConfig);
>
> @@ -51,6 +53,18 @@ my $defaultData = {
> 'pve-sdn-controller-id',
> { completion => \&PVE::Network::SDN::complete_sdn_controller },
> ),
> + 'route-map-in' => {
> + description => "Route Map that should be applied for incoming routes",
> + type => 'string',
> + format => 'pve-sdn-route-map-id',
> + optional => 1,
> + },
> + 'route-map-out' => {
> + description => "Route Map that should be applied for outgoing routes",
> + type => 'string',
> + format => 'pve-sdn-route-map-id',
> + optional => 1,
> + },
> },
> };
>
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 09/13] sdn: generate route map / prefix list configuration on sdn apply
2026-03-25 9:41 ` [PATCH pve-network 09/13] sdn: generate route map / prefix list configuration on sdn apply Stefan Hanreich
@ 2026-03-27 10:47 ` Hannes Laimer
2026-03-27 11:13 ` Stefan Hanreich
0 siblings, 1 reply; 62+ messages in thread
From: Hannes Laimer @ 2026-03-27 10:47 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
`has_pending_changes` should also include checks for route-maps or
prefix-lists
On 2026-03-25 10:44, Stefan Hanreich wrote:
> Commit the newly introduced configuration files to the running
> configuration when applying the SDN configuration, so the FRR config
> generation logic can use them to generate the FRR configuration for
> them.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> src/PVE/Network/SDN.pm | 14 ++++++++++++--
> 1 file changed, 12 insertions(+), 2 deletions(-)
>
> diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
> index 0bb36bf..93b6698 100644
> --- a/src/PVE/Network/SDN.pm
> +++ b/src/PVE/Network/SDN.pm
> @@ -25,6 +25,8 @@ use PVE::Network::SDN::Subnets;
> use PVE::Network::SDN::Dhcp;
> use PVE::Network::SDN::Frr;
> use PVE::Network::SDN::Fabrics;
> +use PVE::Network::SDN::RouteMaps;
> +use PVE::Network::SDN::PrefixLists;
>
> my $RUNNING_CFG_FILENAME = "sdn/.running-config";
>
> @@ -207,12 +209,16 @@ sub compile_running_cfg {
> my $controllers_cfg = PVE::Network::SDN::Controllers::config();
> my $subnets_cfg = PVE::Network::SDN::Subnets::config();
> my $fabrics_cfg = PVE::Network::SDN::Fabrics::config();
> + my $route_maps_cfg = PVE::Network::SDN::RouteMaps::config();
> + my $prefix_lists_cfg = PVE::Network::SDN::PrefixLists::config();
>
> my $vnets = { ids => $vnets_cfg->{ids} };
> my $zones = { ids => $zones_cfg->{ids} };
> my $controllers = { ids => $controllers_cfg->{ids} };
> my $subnets = { ids => $subnets_cfg->{ids} };
> my $fabrics = { ids => $fabrics_cfg->to_sections() };
> + my $route_maps = { ids => $route_maps_cfg->to_sections() };
> + my $prefix_lists = { ids => $prefix_lists_cfg->to_sections() };
>
> $cfg = {
> version => $version,
> @@ -221,6 +227,8 @@ sub compile_running_cfg {
> controllers => $controllers,
> subnets => $subnets,
> fabrics => $fabrics,
> + 'route-maps' => $route_maps,
> + 'prefix-lists' => $prefix_lists,
> };
>
> return $cfg;
> @@ -425,9 +433,11 @@ configuration.
> =cut
>
> sub generate_frr_raw_config {
> - my ($running_config, $fabric_config) = @_;
> + my ($running_config, $fabric_config, $route_map_config, $prefix_list_config) = @_;
>
> $running_config = PVE::Network::SDN::running_config() if !$running_config;
> + $prefix_list_config = PVE::Network::SDN::PrefixLists::config(1) if !$prefix_list_config;
> + $route_map_config = PVE::Network::SDN::RouteMaps::config(1) if !$route_map_config;
> $fabric_config = PVE::Network::SDN::Fabrics::config(1) if !$fabric_config;
>
> my $frr_config = {};
> @@ -438,7 +448,7 @@ sub generate_frr_raw_config {
> my $nodename = PVE::INotify::nodename();
>
> return PVE::RS::SDN::get_frr_raw_config(
> - $frr_config->{'frr'}, $fabric_config, $nodename,
> + $frr_config->{'frr'}, $prefix_list_config, $route_map_config, $fabric_config, $nodename,
> );
> }
>
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 3/9] frr: allow rendering prefix-lists/route-maps separately
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 3/9] frr: allow rendering prefix-lists/route-maps separately Stefan Hanreich
2026-03-25 14:32 ` Gabriel Goller
@ 2026-03-27 10:50 ` Hannes Laimer
2026-03-27 11:34 ` Stefan Hanreich
1 sibling, 1 reply; 62+ messages in thread
From: Hannes Laimer @ 2026-03-27 10:50 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
On 2026-03-25 10:43, Stefan Hanreich wrote:
> This is mainly useful for integration tests for the route maps /
> prefix lists feature in ve-config. It utilizes those methods to render
> route maps and prefix lists in tests, without having to render a full
> FRR configuration.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> proxmox-frr/src/ser/serializer.rs | 35 ++++++++++++++++++++++++++++++-
> 1 file changed, 34 insertions(+), 1 deletion(-)
>
> diff --git a/proxmox-frr/src/ser/serializer.rs b/proxmox-frr/src/ser/serializer.rs
> index 2ac85d8..91aff1f 100644
> --- a/proxmox-frr/src/ser/serializer.rs
> +++ b/proxmox-frr/src/ser/serializer.rs
> @@ -1,7 +1,12 @@
> +use std::collections::BTreeMap;
> +
> use anyhow::Context;
> use minijinja::Environment;
>
> -use crate::ser::FrrConfig;
> +use crate::ser::{
> + route_map::{PrefixListName, PrefixListRule, RouteMapEntry, RouteMapName},
> + FrrConfig,
> +};
> use proxmox_sortable_macro::sortable;
>
> #[sortable]
> @@ -91,3 +96,31 @@ pub fn dump(config: &FrrConfig) -> Result<String, anyhow::Error> {
> pub fn to_raw_config(config: &FrrConfig) -> Result<Vec<String>, anyhow::Error> {
> Ok(dump(config)?.lines().map(|line| line.to_owned()).collect())
> }
> +
> +/// Render Prefix Lists into an FRR config string.
> +///
> +/// Currently only used for testing the rendering of prefix lists, to avoid having to render the
> +/// whole FRR configuration file.
> +pub fn render_prefix_lists(
> + prefix_lists: BTreeMap<PrefixListName, Vec<PrefixListRule>>,
> +) -> Result<String, anyhow::Error> {
> + create_env()
> + .get_template("prefix_lists.jinja")
> + .with_context(|| "could not obtain frr template from environment")?
> + .render(prefix_lists)
the templates expect named keys, no?
```
- .render(prefix_lists)
+ .render(minijinja::context! { prefix_lists => prefix_lists })
```
note: *did not test this*
> + .with_context(|| "could not render frr template")
> +}
> +
> +/// Render Route Maps into an FRR config string.
> +///
> +/// Currently only used for testing the rendering of route maps, to avoid having to render the
> +/// whole FRR configuration file.
> +pub fn render_route_maps(
> + route_maps: BTreeMap<RouteMapName, Vec<RouteMapEntry>>,
> +) -> Result<String, anyhow::Error> {
> + create_env()
> + .get_template("route_maps.jinja")
> + .with_context(|| "could not obtain frr template from environment")?
> + .render(route_maps)
^ same here
> + .with_context(|| "could not render frr template")
> +}
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 4/9] frr-templates: change route maps template to adapt to new types
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 4/9] frr-templates: change route maps template to adapt to new types Stefan Hanreich
2026-03-25 14:33 ` Gabriel Goller
@ 2026-03-27 11:01 ` Hannes Laimer
2026-03-27 11:17 ` Stefan Hanreich
1 sibling, 1 reply; 62+ messages in thread
From: Hannes Laimer @ 2026-03-27 11:01 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
On 2026-03-25 10:41, Stefan Hanreich wrote:
> Instead of defining every potential match / set type manually under a
> different name, proxmox-frr now uses the Adjacently tagged
> representation for representing key/value pairs for match/set actions.
> This allows simplifying the route_maps template by simply rendering
> the respective key / value fields.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
> proxmox-frr-templates/templates/route_maps.jinja | 12 ++++--------
> 1 file changed, 4 insertions(+), 8 deletions(-)
>
> diff --git a/proxmox-frr-templates/templates/route_maps.jinja b/proxmox-frr-templates/templates/route_maps.jinja
> index 172c682..19a6ee4 100644
> --- a/proxmox-frr-templates/templates/route_maps.jinja
> +++ b/proxmox-frr-templates/templates/route_maps.jinja
> @@ -3,16 +3,12 @@
> !
> 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 }}
> -{% elif match.match_type == "next-hop" %}
> - match {{ match.protocol_type }} next-hop {{ match.value }}
> -{% endif %}
> + match {{ match.key }} {% if match.value is defined %} {{ match.value }} {% endif %}
^
I think the space after `match.key`(and before `endif`) is too much, frr
doesn't really care, but still
```
- match {{ match.key }} {% if match.value is defined %} {{ match.value
}} {% endif %}
+ match {{ match.key }}{% if match.value is defined %} {{ match.value
}}{% endif %}
```
> +
> {% endfor %}
> {% for set in routemap.sets %}
> - set {{ set.set_type }} {{ set.value }}
> + set {{ set.key }} {% if set.value is defined %} {{ set.value }} {% endif %}
... same here
> +
> {% endfor %}
> {% for line in routemap.custom_frr_config %}
> {{ line }}
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 08/13] evpn controller: add route_map_{in,out} parameter
2026-03-27 10:44 ` Hannes Laimer
@ 2026-03-27 11:12 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 11:12 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
On 3/27/26 11:43 AM, Hannes Laimer wrote:
> in `generate_zone_frr_config` we probably also want to use the newly
> introduced `route-map-[in|out]` parameter, not the hard coded ones
hmm, that one is actually a bit tricky, at least without my upcoming
patch series that allows defining multiple EVPN controllers with
different route-maps and node constraints, since this needs to be
applied only on the exit nodes,
Not sure if we want to extend the user-defined route map here, since -
depending on the user-defined route-map - we cannot ensure that this
will actually happen. We could instead create a pre-defined route map
here, apply the set actions and then jump into the user-defined route
map. That would kind of defeat the purpose of being able to define
custom route maps - so I'm a bit torn on how to proceed here.
Will have to think more about this...
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH pve-network 09/13] sdn: generate route map / prefix list configuration on sdn apply
2026-03-27 10:47 ` Hannes Laimer
@ 2026-03-27 11:13 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 11:13 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
On 3/27/26 11:46 AM, Hannes Laimer wrote:
> `has_pending_changes` should also include checks for route-maps or
> prefix-lists
yep, fair - and fabrics as well for that matter...
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 8/9] ve-config: frr: implement frr config generation for route maps
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 8/9] ve-config: frr: implement frr config generation for route maps Stefan Hanreich
2026-03-25 15:03 ` Gabriel Goller
@ 2026-03-27 11:17 ` Hannes Laimer
2026-03-27 11:21 ` Stefan Hanreich
1 sibling, 1 reply; 62+ messages in thread
From: Hannes Laimer @ 2026-03-27 11:17 UTC (permalink / raw)
To: Stefan Hanreich, pve-devel
On 2026-03-25 10:42, Stefan Hanreich wrote:
> Implements conversion traits for all the section config types, so they
> can be converted into their respective FRR template counterpart.
>
> This module contains a helper for adding all route map entries to an
> existing FRR configuration. It will overwrite existing route map
> entries that have the same name AND order number. But if entries with
> the same name, but different ordering, exist they will only be added
> to the existing FRR configuration without dropping the other route map
> entries.
> This currently not relevant either way, because the initial API
> implementation will reject creating route maps with names of route
> maps that the stack auto-generates. In the future this behavior can
> be used for selectively overriding / appending existing Proxmox VE
> route maps.
>
> The helper also automatically orders route map entries according to
> their ordering number. This allows for deterministic FRR configuration
> output, which is important for tests and convenient for human
> readability.
>
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
[..]
> + /// Add a list of Route Map Entries to a [`FrrConfig`].
> + ///
> + /// This method takes a list of Route Map Entries and adds them to given FRR configuration.
> + /// Existing Route Map entries with the same name, but different ordering number will remain in
> + /// the configuration. Entries with the same ordering will get merged.
> + ///
> + /// This behavior is different from Prefix Lists, where we overwrite existing Prefix Lists in
> + /// the FRR configuration. The reason for this is that users can override the Route Map setting
> + /// in the EVPN controller.
> + pub fn build_frr_route_maps(
> + config: impl IntoIterator<Item = RouteMap>,
> + frr_config: &mut FrrConfig,
> + ) -> Result<(), anyhow::Error> {
> + for route_map in config.into_iter() {
> + let RouteMap::RouteMapEntry(route_map) = route_map;
> + let route_map_name = RouteMapName::new(route_map.id.route_map_id.to_string());
> +
> + if let Some(frr_route_map) = frr_config.routemaps.get_mut(&route_map_name) {
> + let idx =
> + frr_route_map.partition_point(|element| element.seq <= route_map.id().order());
> + frr_route_map.insert(idx, route_map.into());
with this we would end up having multiple entries with the same seq, frr
doesn't care, just bringing it up cause the doc mentions merging
> + } else {
> + frr_config
> + .routemaps
> + .insert(route_map_name, vec![route_map.into()]);
> + }
> + }
> +
> + Ok(())
> + }
[..]
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 4/9] frr-templates: change route maps template to adapt to new types
2026-03-27 11:01 ` Hannes Laimer
@ 2026-03-27 11:17 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 11:17 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
On 3/27/26 12:00 PM, Hannes Laimer wrote:
> On 2026-03-25 10:41, Stefan Hanreich wrote:
>> Instead of defining every potential match / set type manually under a
>> different name, proxmox-frr now uses the Adjacently tagged
>> representation for representing key/value pairs for match/set actions.
>> This allows simplifying the route_maps template by simply rendering
>> the respective key / value fields.
>>
>> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
>> ---
>> proxmox-frr-templates/templates/route_maps.jinja | 12 ++++--------
>> 1 file changed, 4 insertions(+), 8 deletions(-)
>>
>> diff --git a/proxmox-frr-templates/templates/route_maps.jinja b/proxmox-frr-templates/templates/route_maps.jinja
>> index 172c682..19a6ee4 100644
>> --- a/proxmox-frr-templates/templates/route_maps.jinja
>> +++ b/proxmox-frr-templates/templates/route_maps.jinja
>> @@ -3,16 +3,12 @@
>> !
>> 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 }}
>> -{% elif match.match_type == "next-hop" %}
>> - match {{ match.protocol_type }} next-hop {{ match.value }}
>> -{% endif %}
>> + match {{ match.key }} {% if match.value is defined %} {{ match.value }} {% endif %}
> ^
> I think the space after `match.key`(and before `endif`) is too much, frr
> doesn't really care, but still
> ```
> - match {{ match.key }} {% if match.value is defined %} {{ match.value
> }} {% endif %}
> + match {{ match.key }}{% if match.value is defined %} {{ match.value
> }}{% endif %}
The resulting route maps come out fine on my test cluster (with only one
space) - I'll check more closely nevertheless!
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 8/9] ve-config: frr: implement frr config generation for route maps
2026-03-27 11:17 ` Hannes Laimer
@ 2026-03-27 11:21 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 11:21 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
On 3/27/26 12:16 PM, Hannes Laimer wrote:
[snip]
>> + /// Add a list of Route Map Entries to a [`FrrConfig`].
>> + ///
>> + /// This method takes a list of Route Map Entries and adds them to given FRR configuration.
>> + /// Existing Route Map entries with the same name, but different ordering number will remain in
>> + /// the configuration. Entries with the same ordering will get merged.
>> + ///
>> + /// This behavior is different from Prefix Lists, where we overwrite existing Prefix Lists in
>> + /// the FRR configuration. The reason for this is that users can override the Route Map setting
>> + /// in the EVPN controller.
>> + pub fn build_frr_route_maps(
>> + config: impl IntoIterator<Item = RouteMap>,
>> + frr_config: &mut FrrConfig,
>> + ) -> Result<(), anyhow::Error> {
>> + for route_map in config.into_iter() {
>> + let RouteMap::RouteMapEntry(route_map) = route_map;
>> + let route_map_name = RouteMapName::new(route_map.id.route_map_id.to_string());
>> +
>> + if let Some(frr_route_map) = frr_config.routemaps.get_mut(&route_map_name) {
>> + let idx =
>> + frr_route_map.partition_point(|element| element.seq <= route_map.id().order());
>> + frr_route_map.insert(idx, route_map.into());
>
> with this we would end up having multiple entries with the same seq, frr
> doesn't care, just bringing it up cause the doc mentions merging
yeah, the FRR merging logic is what's being relied upon here, i.e.:
berserker# configure
berserker(config)# route-map test permit 123
berserker(config-route-map)# match evpn vni 1111
berserker(config-route-map)# exit
berserker(config)# route-map test permit 123
berserker(config-route-map)# set metric 1111
berserker(config-route-map)# exit
berserker(config)# exit
berserker# show running-config
[...]
route-map test permit 123
match evpn vni 1111
set metric 1111
exit
!
[...]
------
I'll make sure to make this clearer in the documentation!
^ permalink raw reply [flat|nested] 62+ messages in thread
* Re: [PATCH proxmox-ve-rs 3/9] frr: allow rendering prefix-lists/route-maps separately
2026-03-27 10:50 ` Hannes Laimer
@ 2026-03-27 11:34 ` Stefan Hanreich
0 siblings, 0 replies; 62+ messages in thread
From: Stefan Hanreich @ 2026-03-27 11:34 UTC (permalink / raw)
To: Hannes Laimer, pve-devel
On 3/27/26 11:50 AM, Hannes Laimer wrote:
> On 2026-03-25 10:43, Stefan Hanreich wrote:
>> This is mainly useful for integration tests for the route maps /
>> prefix lists feature in ve-config. It utilizes those methods to render
>> route maps and prefix lists in tests, without having to render a full
>> FRR configuration.
>>
>> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
>> ---
>> proxmox-frr/src/ser/serializer.rs | 35 ++++++++++++++++++++++++++++++-
>> 1 file changed, 34 insertions(+), 1 deletion(-)
>>
>> diff --git a/proxmox-frr/src/ser/serializer.rs b/proxmox-frr/src/ser/serializer.rs
>> index 2ac85d8..91aff1f 100644
>> --- a/proxmox-frr/src/ser/serializer.rs
>> +++ b/proxmox-frr/src/ser/serializer.rs
>> @@ -1,7 +1,12 @@
>> +use std::collections::BTreeMap;
>> +
>> use anyhow::Context;
>> use minijinja::Environment;
>>
>> -use crate::ser::FrrConfig;
>> +use crate::ser::{
>> + route_map::{PrefixListName, PrefixListRule, RouteMapEntry, RouteMapName},
>> + FrrConfig,
>> +};
>> use proxmox_sortable_macro::sortable;
>>
>> #[sortable]
>> @@ -91,3 +96,31 @@ pub fn dump(config: &FrrConfig) -> Result<String, anyhow::Error> {
>> pub fn to_raw_config(config: &FrrConfig) -> Result<Vec<String>, anyhow::Error> {
>> Ok(dump(config)?.lines().map(|line| line.to_owned()).collect())
>> }
>> +
>> +/// Render Prefix Lists into an FRR config string.
>> +///
>> +/// Currently only used for testing the rendering of prefix lists, to avoid having to render the
>> +/// whole FRR configuration file.
>> +pub fn render_prefix_lists(
>> + prefix_lists: BTreeMap<PrefixListName, Vec<PrefixListRule>>,
>> +) -> Result<String, anyhow::Error> {
>> + create_env()
>> + .get_template("prefix_lists.jinja")
>> + .with_context(|| "could not obtain frr template from environment")?
>> + .render(prefix_lists)
>
> the templates expect named keys, no?
> ```
> - .render(prefix_lists)
> + .render(minijinja::context! { prefix_lists => prefix_lists })
> ```
>
they're actually unused and just leftover from development - will be
removed in the upcoming v2, since the tests switched over to using dump
anyway...
^ permalink raw reply [flat|nested] 62+ messages in thread
end of thread, other threads:[~2026-03-27 11:34 UTC | newest]
Thread overview: 62+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-25 9:41 [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-cluster 1/2] cfs: add 'sdn/route-maps.cfg' to observed files Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-cluster 2/2] cfs: add 'sdn/prefix-lists.cfg' " Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 1/9] sdn-types: add common route-map helper types Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 2/9] frr: implement routemap match/set statements via adjacent tagging Stefan Hanreich
2026-03-26 14:44 ` Hannes Laimer
2026-03-27 9:02 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 3/9] frr: allow rendering prefix-lists/route-maps separately Stefan Hanreich
2026-03-25 14:32 ` Gabriel Goller
2026-03-26 12:17 ` Stefan Hanreich
2026-03-27 10:50 ` Hannes Laimer
2026-03-27 11:34 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 4/9] frr-templates: change route maps template to adapt to new types Stefan Hanreich
2026-03-25 14:33 ` Gabriel Goller
2026-03-25 14:58 ` Gabriel Goller
2026-03-27 11:01 ` Hannes Laimer
2026-03-27 11:17 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 5/9] ve-config: add prefix list section config Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 6/9] ve-config: frr: implement frr config generation for prefix lists Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 7/9] ve-config: add route map section config Stefan Hanreich
2026-03-25 14:35 ` Gabriel Goller
2026-03-26 13:49 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 8/9] ve-config: frr: implement frr config generation for route maps Stefan Hanreich
2026-03-25 15:03 ` Gabriel Goller
2026-03-26 13:50 ` Stefan Hanreich
2026-03-27 11:17 ` Hannes Laimer
2026-03-27 11:21 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-ve-rs 9/9] ve-config: fabrics: adapt frr config generation to new format Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-perl-rs 1/3] pve-rs: sdn: add route maps module Stefan Hanreich
2026-03-26 10:32 ` Wolfgang Bumiller
2026-03-26 13:57 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-perl-rs 2/3] pve-rs: sdn: add prefix lists module Stefan Hanreich
2026-03-25 9:41 ` [PATCH proxmox-perl-rs 3/3] sdn: add prefix list / route maps to frr config generation helper Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 01/13] controller: bgp: evpn: adapt to new match / set frr config syntax Stefan Hanreich
2026-03-26 15:19 ` Hannes Laimer
2026-03-27 10:05 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 02/13] sdn: add prefix lists module Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 03/13] api2: add prefix list module Stefan Hanreich
2026-03-26 15:01 ` Hannes Laimer
2026-03-27 9:57 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 04/13] sdn: add route map module Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 05/13] api2: add route maps api module Stefan Hanreich
2026-03-26 15:05 ` Hannes Laimer
2026-03-27 9:57 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 06/13] api2: add route map module Stefan Hanreich
2026-03-26 15:07 ` Hannes Laimer
2026-03-27 9:57 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 07/13] api2: add route map entry module Stefan Hanreich
2026-03-26 15:13 ` Hannes Laimer
2026-03-27 10:01 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 08/13] evpn controller: add route_map_{in,out} parameter Stefan Hanreich
2026-03-27 10:44 ` Hannes Laimer
2026-03-27 11:12 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 09/13] sdn: generate route map / prefix list configuration on sdn apply Stefan Hanreich
2026-03-27 10:47 ` Hannes Laimer
2026-03-27 11:13 ` Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 10/13] tests: add simple route map test case Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 11/13] tests: add bgp evpn route map/prefix list testcase Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 12/13] tests: add route map with prefix " Stefan Hanreich
2026-03-25 9:41 ` [PATCH pve-network 13/13] bgp controller: allow configuring custom route maps Stefan Hanreich
2026-03-25 11:38 ` [PATCH cluster/network/proxmox{-ve-rs,-perl-rs} 00/27] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-03-27 10:17 ` Stefan Hanreich
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox