From: Gabriel Goller <g.goller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ve-rs 5/9] frr: add template serializer and serialize fabrics using templates
Date: Tue, 3 Feb 2026 17:01:12 +0100 [thread overview]
Message-ID: <20260203160246.353351-6-g.goller@proxmox.com> (raw)
In-Reply-To: <20260203160246.353351-1-g.goller@proxmox.com>
Add a new serializer which uses templates in
`/etc/proxmox-frr/templates` or from the `proxmox-frr-templates` package
in `/usr/share/proxmox-frr/templates` to generate the frr config file.
Also update the `build_fabric` function and the tests accordingly.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr/Cargo.toml | 3 +
proxmox-frr/debian/control | 10 +
proxmox-frr/src/ser/mod.rs | 247 ++++++--------
proxmox-frr/src/ser/openfabric.rs | 26 +-
proxmox-frr/src/ser/ospf.rs | 56 +---
proxmox-frr/src/ser/route_map.rs | 175 ++++------
proxmox-frr/src/ser/serializer.rs | 259 ++++-----------
proxmox-sdn-types/src/net.rs | 4 +-
proxmox-ve-config/src/common/valid.rs | 4 +-
proxmox-ve-config/src/sdn/fabric/frr.rs | 302 ++++++++++--------
.../fabric__openfabric_default_pve.snap | 2 +-
.../fabric__openfabric_default_pve1.snap | 2 +-
.../fabric__openfabric_dualstack_pve.snap | 13 +-
.../fabric__openfabric_ipv6_only_pve.snap | 4 +-
.../fabric__openfabric_multi_fabric_pve1.snap | 2 +-
.../snapshots/fabric__ospf_default_pve.snap | 2 +-
.../snapshots/fabric__ospf_default_pve1.snap | 2 +-
.../fabric__ospf_multi_fabric_pve1.snap | 2 +-
18 files changed, 468 insertions(+), 647 deletions(-)
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
index 31e93395cd20..560a04b42980 100644
--- a/proxmox-frr/Cargo.toml
+++ b/proxmox-frr/Cargo.toml
@@ -15,6 +15,9 @@ anyhow = "1"
tracing = "0.1"
serde = { workspace = true, features = [ "derive" ] }
serde_repr = "0.1"
+minijinja = { version = "2.5", features = [ "multi_template", "loader" ] }
+bon = "3.7"
proxmox-network-types = { workspace = true }
proxmox-sdn-types = { workspace = true }
+proxmox-serde = { workspace = true }
diff --git a/proxmox-frr/debian/control b/proxmox-frr/debian/control
index 6336ec362b45..10651e640ba3 100644
--- a/proxmox-frr/debian/control
+++ b/proxmox-frr/debian/control
@@ -7,8 +7,13 @@ Build-Depends-Arch: cargo:native <!nocheck>,
rustc:native (>= 1.82) <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
+ librust-bon-3+default-dev (>= 3.7-~~) <!nocheck>,
+ librust-minijinja-2+default-dev (>= 2.5-~~) <!nocheck>,
+ librust-minijinja-2+loader-dev (>= 2.5-~~) <!nocheck>,
+ librust-minijinja-2+multi-template-dev (>= 2.5-~~) <!nocheck>,
librust-proxmox-network-types-0.1+default-dev (>= 0.1.1-~~) <!nocheck>,
librust-proxmox-sdn-types-0.1+default-dev <!nocheck>,
+ librust-proxmox-serde-1+default-dev <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>,
librust-serde-repr-0.1+default-dev <!nocheck>,
@@ -27,8 +32,13 @@ Multi-Arch: same
Depends:
${misc:Depends},
librust-anyhow-1+default-dev,
+ librust-bon-3+default-dev (>= 3.7-~~),
+ librust-minijinja-2+default-dev (>= 2.5-~~),
+ librust-minijinja-2+loader-dev (>= 2.5-~~),
+ librust-minijinja-2+multi-template-dev (>= 2.5-~~),
librust-proxmox-network-types-0.1+default-dev (>= 0.1.1-~~),
librust-proxmox-sdn-types-0.1+default-dev,
+ librust-proxmox-serde-1+default-dev,
librust-serde-1+default-dev,
librust-serde-1+derive-dev,
librust-serde-repr-0.1+default-dev,
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
index a90397b59a9b..666845ecab74 100644
--- a/proxmox-frr/src/ser/mod.rs
+++ b/proxmox-frr/src/ser/mod.rs
@@ -3,104 +3,17 @@ pub mod ospf;
pub mod route_map;
pub mod serializer;
-use std::collections::{BTreeMap, BTreeSet};
-use std::fmt::Display;
+use std::collections::BTreeMap;
+use std::net::IpAddr;
use std::str::FromStr;
-use crate::ser::route_map::{AccessList, ProtocolRouteMap, RouteMap};
+use crate::ser::route_map::{AccessListName, AccessListRule, RouteMapEntry, RouteMapName};
+use bon::Builder;
+use proxmox_network_types::ip_address::Cidr;
+use serde::{Deserialize, Serialize};
use thiserror::Error;
-/// Generic FRR router.
-///
-/// This generic FRR router contains all the protocols that we implement.
-/// In FRR this is e.g.:
-/// ```text
-/// router openfabric test
-/// !....
-/// ! or
-/// router ospf
-/// !....
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum Router {
- Openfabric(openfabric::OpenfabricRouter),
- Ospf(ospf::OspfRouter),
-}
-
-impl From<openfabric::OpenfabricRouter> for Router {
- fn from(value: openfabric::OpenfabricRouter) -> Self {
- Router::Openfabric(value)
- }
-}
-
-/// Generic FRR routername.
-///
-/// The variants represent different protocols. Some have `router <protocol> <name>`, others have
-/// `router <protocol> <process-id>`, some only have `router <protocol>`.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum RouterName {
- Openfabric(openfabric::OpenfabricRouterName),
- Ospf(ospf::OspfRouterName),
-}
-
-impl From<openfabric::OpenfabricRouterName> for RouterName {
- fn from(value: openfabric::OpenfabricRouterName) -> Self {
- Self::Openfabric(value)
- }
-}
-
-impl Display for RouterName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Openfabric(r) => r.fmt(f),
- Self::Ospf(r) => r.fmt(f),
- }
- }
-}
-
-/// The interface name is the same on ospf and openfabric, but it is an enum so that we can have
-/// two different entries in the btreemap. This allows us to have an interface in a ospf and
-/// openfabric fabric.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum InterfaceName {
- Openfabric(CommonInterfaceName),
- Ospf(CommonInterfaceName),
-}
-
-impl Display for InterfaceName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- InterfaceName::Openfabric(frr_word) => frr_word.fmt(f),
- InterfaceName::Ospf(frr_word) => frr_word.fmt(f),
- }
- }
-}
-
-/// Generic FRR Interface.
-///
-/// In FRR config it looks like this:
-/// ```text
-/// interface <name>
-/// ! ...
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
-pub enum Interface {
- Openfabric(openfabric::OpenfabricInterface),
- Ospf(ospf::OspfInterface),
-}
-
-impl From<openfabric::OpenfabricInterface> for Interface {
- fn from(value: openfabric::OpenfabricInterface) -> Self {
- Self::Openfabric(value)
- }
-}
-
-impl From<ospf::OspfInterface> for Interface {
- fn from(value: ospf::OspfInterface) -> Self {
- Self::Ospf(value)
- }
-}
-
#[derive(Error, Debug)]
pub enum FrrWordError {
#[error("word is empty")]
@@ -113,7 +26,7 @@ pub enum FrrWordError {
///
/// Every string argument or value in FRR is an FrrWord. FrrWords must only contain ascii
/// characters and must not have a whitespace.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct FrrWord(String);
impl FrrWord {
@@ -144,12 +57,6 @@ impl FromStr for FrrWord {
}
}
-impl Display for FrrWord {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
- }
-}
-
impl AsRef<str> for FrrWord {
fn as_ref(&self) -> &str {
&self.0
@@ -157,7 +64,7 @@ impl AsRef<str> for FrrWord {
}
#[derive(Error, Debug)]
-pub enum CommonInterfaceNameError {
+pub enum InterfaceNameError {
#[error("interface name too long")]
TooLong,
}
@@ -166,76 +73,128 @@ pub enum CommonInterfaceNameError {
///
/// FRR itself doesn't enforce any limits, but the kernel does. Linux only allows interface names
/// to be a maximum of 16 bytes. This is enforced by this struct.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct CommonInterfaceName(String);
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct InterfaceName(String);
-impl TryFrom<&str> for CommonInterfaceName {
- type Error = CommonInterfaceNameError;
+impl TryFrom<&str> for InterfaceName {
+ type Error = InterfaceNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
-impl TryFrom<String> for CommonInterfaceName {
- type Error = CommonInterfaceNameError;
+impl TryFrom<String> for InterfaceName {
+ type Error = InterfaceNameError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
-impl CommonInterfaceName {
- pub fn new<T: AsRef<str> + Into<String>>(s: T) -> Result<Self, CommonInterfaceNameError> {
+impl InterfaceName {
+ pub fn new<T: AsRef<str> + Into<String>>(s: T) -> Result<Self, InterfaceNameError> {
if s.as_ref().len() <= 15 {
Ok(Self(s.into()))
} else {
- Err(CommonInterfaceNameError::TooLong)
+ Err(InterfaceNameError::TooLong)
}
}
}
-impl Display for CommonInterfaceName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
+#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)]
+pub struct Interface<T> {
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub addresses: Vec<Cidr>,
+
+ #[serde(flatten)]
+ pub properties: T,
+}
+impl From<openfabric::OpenfabricInterface> for Interface<openfabric::OpenfabricInterface> {
+ fn from(value: openfabric::OpenfabricInterface) -> Self {
+ Interface {
+ addresses: Vec::new(),
+ properties: value,
+ }
}
}
-/// Main FRR config.
-///
-/// Contains the two main frr building blocks: routers and interfaces. It also holds other
-/// top-level FRR options, such as access-lists, router-maps and protocol-routemaps. This struct
-/// gets generated using the `FrrConfigBuilder` in `proxmox-ve-config`.
-#[derive(Clone, Debug, PartialEq, Eq, Default)]
-pub struct FrrConfig {
- pub router: BTreeMap<RouterName, Router>,
- pub interfaces: BTreeMap<InterfaceName, Interface>,
- pub access_lists: Vec<AccessList>,
- pub routemaps: Vec<RouteMap>,
- pub protocol_routemaps: BTreeSet<ProtocolRouteMap>,
+impl From<ospf::OspfInterface> for Interface<ospf::OspfInterface> {
+ fn from(value: ospf::OspfInterface) -> Self {
+ Interface {
+ addresses: Vec::new(),
+ properties: value,
+ }
+ }
}
-impl FrrConfig {
- pub fn new() -> Self {
- Self::default()
- }
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum IpOrInterface {
+ Ip(IpAddr),
+ Interface(InterfaceName),
+}
- pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
- self.router.iter()
- }
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)]
+pub struct IpRoute {
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ is_ipv6: bool,
+ prefix: Cidr,
+ via: IpOrInterface,
+ vrf: Option<InterfaceName>,
+}
- pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
- self.interfaces.iter()
- }
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum FrrProtocol {
+ Ospf,
+ Openfabric,
+ Bgp,
+}
- pub fn access_lists(&self) -> impl Iterator<Item = &AccessList> + '_ {
- self.access_lists.iter()
- }
- pub fn routemaps(&self) -> impl Iterator<Item = &RouteMap> + '_ {
- self.routemaps.iter()
- }
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct IpProtocolRouteMap {
+ pub v4: Option<RouteMapName>,
+ pub v6: Option<RouteMapName>,
+}
- pub fn protocol_routemaps(&self) -> impl Iterator<Item = &ProtocolRouteMap> + '_ {
- self.protocol_routemaps.iter()
- }
+/// Main FRR config.
+///
+/// Contains the two main frr building blocks: routers and interfaces. It also holds other
+/// top-level FRR options, such as access-lists, router-maps and protocol-routemaps. This struct
+/// gets generated using the `FrrConfigBuilder` in `proxmox-ve-config`.
+#[derive(Clone, Debug, PartialEq, Eq, Default, Builder, Serialize, Deserialize)]
+pub struct FrrConfig {
+ #[builder(default)]
+ #[serde(default)]
+ pub openfabric: OpenfabricFrrConfig,
+ #[builder(default)]
+ #[serde(default)]
+ pub ospf: OspfFrrConfig,
+ #[builder(default)]
+ #[serde(default)]
+ pub protocol_routemaps: BTreeMap<FrrProtocol, IpProtocolRouteMap>,
+
+ #[builder(default)]
+ #[serde(default)]
+ pub routemaps: BTreeMap<RouteMapName, Vec<RouteMapEntry>>,
+ #[builder(default)]
+ #[serde(default)]
+ pub access_lists: BTreeMap<AccessListName, Vec<AccessListRule>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub struct OpenfabricFrrConfig {
+ #[serde(default)]
+ pub router: BTreeMap<openfabric::OpenfabricRouterName, openfabric::OpenfabricRouter>,
+ #[serde(default)]
+ pub interfaces: BTreeMap<InterfaceName, Interface<openfabric::OpenfabricInterface>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub struct OspfFrrConfig {
+ #[serde(default)]
+ pub router: Option<ospf::OspfRouter>,
+ #[serde(default)]
+ pub interfaces: BTreeMap<InterfaceName, Interface<ospf::OspfInterface>>,
}
diff --git a/proxmox-frr/src/ser/openfabric.rs b/proxmox-frr/src/ser/openfabric.rs
index 0f0c65062d36..8c82c60b0de5 100644
--- a/proxmox-frr/src/ser/openfabric.rs
+++ b/proxmox-frr/src/ser/openfabric.rs
@@ -1,15 +1,17 @@
use std::fmt::Debug;
-use std::fmt::Display;
+use bon::Builder;
use proxmox_sdn_types::net::Net;
+use serde::Deserialize;
+use serde::Serialize;
use thiserror::Error;
use crate::ser::FrrWord;
use crate::ser::FrrWordError;
/// The name of a OpenFabric router. Is an FrrWord.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct OpenfabricRouterName(FrrWord);
impl From<FrrWord> for OpenfabricRouterName {
@@ -24,16 +26,16 @@ impl OpenfabricRouterName {
}
}
-impl Display for OpenfabricRouterName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "openfabric {}", self.0)
+impl std::fmt::Display for OpenfabricRouterName {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ self.0.fmt(f)
}
}
/// All the properties a OpenFabric router can hold.
///
/// These can serialized with a " " space prefix as they are in the `router openfabric` block.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct OpenfabricRouter {
/// The NET address
pub net: Net,
@@ -67,15 +69,25 @@ impl OpenfabricRouter {
///
/// The is_ipv4 and is_ipv6 properties decide if we need to add `ip router openfabric`, `ipv6
/// router openfabric`, or both. An interface can only be part of a single fabric.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Builder)]
pub struct OpenfabricInterface {
// Note: an interface can only be a part of a single fabric (so no vec needed here)
pub fabric_id: OpenfabricRouterName,
+ #[serde(
+ default,
+ deserialize_with = "proxmox_serde::perl::deserialize_bool",
+ skip_serializing_if = "Option::is_none"
+ )]
pub passive: Option<bool>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub hello_interval: Option<proxmox_sdn_types::openfabric::HelloInterval>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub csnp_interval: Option<proxmox_sdn_types::openfabric::CsnpInterval>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub hello_multiplier: Option<proxmox_sdn_types::openfabric::HelloMultiplier>,
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
pub is_ipv4: bool,
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
pub is_ipv6: bool,
}
diff --git a/proxmox-frr/src/ser/ospf.rs b/proxmox-frr/src/ser/ospf.rs
index 67e39a45b8de..8b26f42e2e46 100644
--- a/proxmox-frr/src/ser/ospf.rs
+++ b/proxmox-frr/src/ser/ospf.rs
@@ -1,32 +1,12 @@
use std::fmt::Debug;
-use std::fmt::Display;
use std::net::Ipv4Addr;
+use bon::Builder;
+use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::ser::{FrrWord, FrrWordError};
-/// The name of the ospf frr router.
-///
-/// We can only have a single ospf router (ignoring multiple invocations of the ospfd daemon)
-/// because the router-id needs to be the same between different routers on a single node.
-/// We can still have multiple fabrics by separating them using areas. Still, different areas have
-/// the same frr router, so the name of the router is just "ospf" in "router ospf".
-///
-/// This serializes roughly to:
-/// ```text
-/// router ospf
-/// !...
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct OspfRouterName;
-
-impl Display for OspfRouterName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "ospf")
- }
-}
-
#[derive(Error, Debug)]
pub enum AreaParsingError {
#[error("Invalid area idenitifier. Area must be a number or an ipv4 address.")]
@@ -44,7 +24,7 @@ pub enum AreaParsingError {
/// or "0" as an area, which then gets translated to "0.0.0.5" and "0.0.0.0" by FRR. We allow both
/// a number or an ip-address. Note that the area "0" (or "0.0.0.0") is a special area - it creates
/// a OSPF "backbone" area.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Area(FrrWord);
impl TryFrom<FrrWord> for Area {
@@ -65,12 +45,6 @@ impl Area {
}
}
-impl Display for Area {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "area {}", self.0)
- }
-}
-
/// The OSPF router properties.
///
/// Currently the only property of a OSPF router is the router_id. The router_id is used to
@@ -84,7 +58,7 @@ impl Display for Area {
/// router ospf
/// router-id <ipv4-address>
/// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct OspfRouter {
pub router_id: Ipv4Addr,
}
@@ -119,7 +93,8 @@ pub enum OspfInterfaceError {
/// ! or
/// ip ospf network broadcast
/// ```
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
pub enum NetworkType {
Broadcast,
NonBroadcast,
@@ -132,17 +107,6 @@ pub enum NetworkType {
PointToMultipoint,
}
-impl Display for NetworkType {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- NetworkType::Broadcast => write!(f, "broadcast"),
- NetworkType::NonBroadcast => write!(f, "non-broadcast"),
- NetworkType::PointToPoint => write!(f, "point-to-point"),
- NetworkType::PointToMultipoint => write!(f, "point-to-multicast"),
- }
- }
-}
-
/// The OSPF interface properties.
///
/// The interface gets tied to its fabric by the area property and the FRR `ip ospf area <area>`
@@ -156,10 +120,16 @@ impl Display for NetworkType {
/// ip ospf passive <value>
/// ip ospf network <value>
/// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Builder)]
pub struct OspfInterface {
// Note: an interface can only be a part of a single area(so no vec needed here)
pub area: Area,
+ #[serde(
+ default,
+ skip_serializing_if = "Option::is_none",
+ deserialize_with = "proxmox_serde::perl::deserialize_bool"
+ )]
pub passive: Option<bool>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub network_type: Option<NetworkType>,
}
diff --git a/proxmox-frr/src/ser/route_map.rs b/proxmox-frr/src/ser/route_map.rs
index 0918a3cead14..995e21655cd0 100644
--- a/proxmox-frr/src/ser/route_map.rs
+++ b/proxmox-frr/src/ser/route_map.rs
@@ -1,29 +1,19 @@
-use std::{
- fmt::{self, Display},
- net::IpAddr,
-};
+use std::net::IpAddr;
use proxmox_network_types::ip_address::Cidr;
+use serde::{Deserialize, Serialize};
/// The action for a [`AccessListRule`].
///
/// The default is Permit. Deny can be used to create a NOT match (e.g. match all routes that are
/// NOT in 10.10.10.0/24 using `ip access-list TEST deny 10.10.10.0/24`).
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
pub enum AccessAction {
Permit,
Deny,
}
-impl fmt::Display for AccessAction {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- AccessAction::Permit => write!(f, "permit"),
- AccessAction::Deny => write!(f, "deny"),
- }
- }
-}
-
/// A single [`AccessList`] rule.
///
/// Every rule in a [`AccessList`] is its own command and gets written into a new line (with the
@@ -40,29 +30,29 @@ impl fmt::Display for AccessAction {
/// ! or
/// ipv6 access-list filter permit 2001:db8::/64
/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccessListRule {
pub action: AccessAction,
pub network: Cidr,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub seq: Option<u32>,
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub is_ipv6: bool,
}
/// The name of an [`AccessList`].
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct AccessListName(String);
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct PrefixListName(String);
+
impl AccessListName {
pub fn new(name: String) -> AccessListName {
AccessListName(name)
}
}
-impl Display for AccessListName {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
- }
-}
-
/// A FRR access-list.
///
/// Holds a vec of rules. Each rule will get its own line, FRR will collect all the rules with the
@@ -75,12 +65,29 @@ impl Display for AccessListName {
/// ip access-list pve_test permit 12.1.1.0/24
/// ip access-list pve_test deny 8.8.8.8/32
/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccessList {
pub name: AccessListName,
pub rules: Vec<AccessListRule>,
}
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct PrefixList {
+ pub name: PrefixListName,
+ pub rules: Vec<PrefixListRule>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct PrefixListRule {
+ pub action: AccessAction,
+ pub network: Cidr,
+ pub seq: Option<u32>,
+ pub le: Option<u32>,
+ pub ge: Option<u32>,
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ pub is_ipv6: bool,
+}
+
/// A match statement inside a route-map.
///
/// A route-map has one or more match statements which decide on which routes the route-map will
@@ -98,66 +105,58 @@ pub struct AccessList {
/// ! or
/// match ipv6 next-hop <ip-address>
/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(tag = "protocol_type")]
pub enum RouteMapMatch {
+ #[serde(rename = "ip")]
V4(RouteMapMatchInner),
+ #[serde(rename = "ipv6")]
V6(RouteMapMatchInner),
+ Vni(u32),
}
-impl Display for RouteMapMatch {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- RouteMapMatch::V4(route_map_match_v4) => match route_map_match_v4 {
- RouteMapMatchInner::IpAddress(access_list_name) => {
- write!(f, "match ip address {access_list_name}")
- }
- RouteMapMatchInner::IpNextHop(next_hop) => {
- write!(f, "match ip next-hop {next_hop}")
- }
- },
- RouteMapMatch::V6(route_map_match_v6) => match route_map_match_v6 {
- RouteMapMatchInner::IpAddress(access_list_name) => {
- write!(f, "match ipv6 address {access_list_name}")
- }
- RouteMapMatchInner::IpNextHop(next_hop) => {
- write!(f, "match ipv6 next-hop {next_hop}")
- }
- },
- }
- }
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(tag = "list_type", content = "list_name", rename_all = "lowercase")]
+pub enum AccessListOrPrefixList {
+ PrefixList(PrefixListName),
+ AccessList(AccessListName),
}
/// A route-map match statement generic on the IP-version.
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(tag = "match_type", content = "value", rename_all = "kebab-case")]
pub enum RouteMapMatchInner {
- IpAddress(AccessListName),
- IpNextHop(String),
+ Address(AccessListOrPrefixList),
+ NextHop(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum SetIpNextHopValue {
+ PeerAddress,
+ Unchanged,
+ IpAddr(IpAddr),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum SetTagValue {
+ Untagged,
+ Numeric(u32),
}
/// Defines the Action a route-map takes when it matches on a route.
///
/// If the route matches the [`RouteMapMatch`], then a [`RouteMapSet`] action will be executed.
/// We currently only use the IpSrc command which changes the source address of the route.
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(tag = "set_type", content = "value", rename_all = "kebab-case")]
pub enum RouteMapSet {
LocalPreference(u32),
- IpSrc(IpAddr),
+ Src(IpAddr),
Metric(u32),
Community(String),
}
-impl Display for RouteMapSet {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- RouteMapSet::LocalPreference(pref) => write!(f, "set local-preference {}", pref),
- RouteMapSet::IpSrc(addr) => write!(f, "set src {}", addr),
- RouteMapSet::Metric(metric) => write!(f, "set metric {}", metric),
- RouteMapSet::Community(community) => write!(f, "set community {}", community),
- }
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct RouteMapName(String);
impl RouteMapName {
@@ -166,12 +165,6 @@ impl RouteMapName {
}
}
-impl Display for RouteMapName {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
- }
-}
-
/// A FRR route-map.
///
/// In FRR route-maps are used to manipulate routes learned by protocols. We can match on specific
@@ -186,48 +179,14 @@ impl Display for RouteMapName {
/// set src <ip-address>
/// exit
/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RouteMap {
- pub name: RouteMapName,
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RouteMapEntry {
pub seq: u32,
pub action: AccessAction,
+ #[serde(default)]
pub matches: Vec<RouteMapMatch>,
+ #[serde(default)]
pub sets: Vec<RouteMapSet>,
-}
-
-/// The ProtocolType used in the [`ProtocolRouteMap`].
-///
-/// Specifies to which protocols we can attach route-maps.
-#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum ProtocolType {
- Openfabric,
- Ospf,
-}
-
-impl Display for ProtocolType {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- ProtocolType::Openfabric => write!(f, "openfabric"),
- ProtocolType::Ospf => write!(f, "ospf"),
- }
- }
-}
-
-/// ProtocolRouteMap statement.
-///
-/// This statement attaches the route-map to the protocol, so that all the routes learned through
-/// the specified protocol can be matched on and manipulated with the route-map.
-///
-/// This serializes to:
-///
-/// ```text
-/// ip protocol <protocol> route-map <route-map-name>
-/// ! or
-/// ipv6 protocol <protocol> route-map <route-map-name>
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct ProtocolRouteMap {
- pub is_ipv6: bool,
- pub protocol: ProtocolType,
- pub routemap_name: RouteMapName,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub custom_frr_config: Vec<String>,
}
diff --git a/proxmox-frr/src/ser/serializer.rs b/proxmox-frr/src/ser/serializer.rs
index 3a681e2f0d7a..da7a31b7cbf6 100644
--- a/proxmox-frr/src/ser/serializer.rs
+++ b/proxmox-frr/src/ser/serializer.rs
@@ -1,203 +1,66 @@
-use std::fmt::{self, Write};
-
-use crate::ser::{
- openfabric::{OpenfabricInterface, OpenfabricRouter},
- ospf::{OspfInterface, OspfRouter},
- route_map::{AccessList, AccessListName, ProtocolRouteMap, RouteMap},
- FrrConfig, Interface, InterfaceName, Router, RouterName,
-};
-
-pub struct FrrConfigBlob<'a> {
- buf: &'a mut (dyn Write + 'a),
-}
-
-impl Write for FrrConfigBlob<'_> {
- fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
- self.buf.write_str(s)
- }
-}
-
-pub trait FrrSerializer {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result;
-}
-
-pub fn to_raw_config(frr_config: &FrrConfig) -> Result<Vec<String>, anyhow::Error> {
- let mut out = String::new();
- let mut blob = FrrConfigBlob { buf: &mut out };
- frr_config.serialize(&mut blob)?;
-
- Ok(out.as_str().lines().map(String::from).collect())
-}
-
-pub fn dump(config: &FrrConfig) -> Result<String, anyhow::Error> {
- let mut out = String::new();
- let mut blob = FrrConfigBlob { buf: &mut out };
- config.serialize(&mut blob)?;
- Ok(out)
-}
-
-impl FrrSerializer for FrrConfig {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- self.router().try_for_each(|router| router.serialize(f))?;
- self.interfaces()
- .try_for_each(|interface| interface.serialize(f))?;
- self.access_lists().try_for_each(|list| list.serialize(f))?;
- self.routemaps().try_for_each(|map| map.serialize(f))?;
- self.protocol_routemaps()
- .try_for_each(|pm| pm.serialize(f))?;
- Ok(())
- }
-}
-
-impl FrrSerializer for (&RouterName, &Router) {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- let router_name = self.0;
- let router = self.1;
- writeln!(f, "router {router_name}")?;
- router.serialize(f)?;
- writeln!(f, "exit")?;
- writeln!(f, "!")?;
- Ok(())
- }
-}
-
-impl FrrSerializer for (&InterfaceName, &Interface) {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- let interface_name = self.0;
- let interface = self.1;
- writeln!(f, "interface {interface_name}")?;
- interface.serialize(f)?;
- writeln!(f, "exit")?;
- writeln!(f, "!")?;
- Ok(())
- }
-}
-
-impl FrrSerializer for (&AccessListName, &AccessList) {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- self.1.serialize(f)?;
- writeln!(f, "!")
- }
-}
-
-impl FrrSerializer for Interface {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- match self {
- Interface::Openfabric(openfabric_interface) => openfabric_interface.serialize(f)?,
- Interface::Ospf(ospf_interface) => ospf_interface.serialize(f)?,
+use std::{fs, path::PathBuf};
+
+use anyhow::Context;
+use minijinja::Environment;
+
+use crate::ser::FrrConfig;
+
+fn create_env<'a>() -> Environment<'a> {
+ let mut env = Environment::new();
+
+ // avoid unnecessary additional newlines
+ env.set_trim_blocks(true);
+ env.set_lstrip_blocks(true);
+
+ env.set_loader(move |name| {
+ let override_path = PathBuf::from(format!("/etc/proxmox-frr/templates/{name}"));
+ // first read the override template:
+ match fs::read_to_string(override_path) {
+ Ok(template_content) => Ok(Some(template_content)),
+ // if that fails, read the vendored template:
+ Err(_) => match name {
+ "fabricd.jinja" => Ok(Some(
+ include_str!("/usr/share/proxmox-frr/templates/fabricd.jinja").to_owned(),
+ )),
+ "ospfd.jinja" => Ok(Some(
+ include_str!("/usr/share/proxmox-frr/templates/ospfd.jinja").to_owned(),
+ )),
+ "interface.jinja" => Ok(Some(
+ include_str!("/usr/share/proxmox-frr/templates/interface.jinja").to_owned(),
+ )),
+ "access_lists.jinja" => Ok(Some(
+ include_str!("/usr/share/proxmox-frr/templates/access_lists.jinja").to_owned(),
+ )),
+ "route_maps.jinja" => Ok(Some(
+ include_str!("/usr/share/proxmox-frr/templates/route_maps.jinja").to_owned(),
+ )),
+ "protocol_routemaps.jinja" => Ok(Some(
+ include_str!("/usr/share/proxmox-frr/templates/protocol_routemaps.jinja")
+ .to_owned(),
+ )),
+ "frr.conf.jinja" => Ok(Some(
+ include_str!("/usr/share/proxmox-frr/templates/frr.conf.jinja").to_owned(),
+ )),
+ _ => Ok(None),
+ },
}
- Ok(())
- }
-}
+ });
-impl FrrSerializer for OpenfabricInterface {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- if self.is_ipv6 {
- writeln!(f, " ipv6 router {}", self.fabric_id)?;
- }
- if self.is_ipv4 {
- writeln!(f, " ip router {}", self.fabric_id)?;
- }
- if self.passive == Some(true) {
- writeln!(f, " openfabric passive")?;
- }
- if let Some(interval) = self.hello_interval {
- writeln!(f, " openfabric hello-interval {interval}",)?;
- }
- if let Some(multiplier) = self.hello_multiplier {
- writeln!(f, " openfabric hello-multiplier {multiplier}",)?;
- }
- if let Some(interval) = self.csnp_interval {
- writeln!(f, " openfabric csnp-interval {interval}",)?;
- }
- Ok(())
- }
-}
-
-impl FrrSerializer for OspfInterface {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- writeln!(f, " ip ospf {}", self.area)?;
- if self.passive == Some(true) {
- writeln!(f, " ip ospf passive")?;
- }
- if let Some(network_type) = &self.network_type {
- writeln!(f, " ip ospf network {network_type}")?;
- }
- Ok(())
- }
-}
-
-impl FrrSerializer for Router {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- match self {
- Router::Openfabric(open_fabric_router) => open_fabric_router.serialize(f),
- Router::Ospf(ospf_router) => ospf_router.serialize(f),
- }
- }
-}
-
-impl FrrSerializer for OpenfabricRouter {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- writeln!(f, " net {}", self.net())?;
- Ok(())
- }
-}
-
-impl FrrSerializer for OspfRouter {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- writeln!(f, " ospf router-id {}", self.router_id())?;
- Ok(())
- }
-}
-
-impl FrrSerializer for AccessList {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- for i in &self.rules {
- if i.network.is_ipv6() {
- write!(f, "ipv6 ")?;
- }
- write!(f, "access-list {} ", self.name)?;
- if let Some(seq) = i.seq {
- write!(f, "seq {seq} ")?;
- }
- write!(f, "{} ", i.action)?;
- writeln!(f, "{}", i.network)?;
- }
- writeln!(f, "!")?;
- Ok(())
- }
+ env
}
-impl FrrSerializer for RouteMap {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- writeln!(f, "route-map {} {} {}", self.name, self.action, self.seq)?;
- for i in &self.matches {
- writeln!(f, " {}", i)?;
- }
- for i in &self.sets {
- writeln!(f, " {}", i)?;
- }
- writeln!(f, "exit")?;
- writeln!(f, "!")
- }
-}
-
-impl FrrSerializer for ProtocolRouteMap {
- fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
- if self.is_ipv6 {
- writeln!(
- f,
- "ipv6 protocol {} route-map {}",
- self.protocol, self.routemap_name
- )?;
- } else {
- writeln!(
- f,
- "ip protocol {} route-map {}",
- self.protocol, self.routemap_name
- )?;
- }
- writeln!(f, "!")?;
- Ok(())
- }
+/// Render the passed [`FrrConfig`] into a single string containing the whole config.
+pub fn dump(config: &FrrConfig) -> Result<String, anyhow::Error> {
+ create_env()
+ .get_template("frr.conf.jinja")
+ .with_context(|| "could not obtain frr template from environment")?
+ .render(config)
+ .with_context(|| "could not render frr template")
+}
+
+/// Render the passed [`FrrConfig`] into the literal Frr config.
+///
+/// The Frr config is returned as lines stored in a Vec.
+pub fn to_raw_config(config: &FrrConfig) -> Result<Vec<String>, anyhow::Error> {
+ Ok(dump(config)?.lines().map(|line| line.to_owned()).collect())
}
diff --git a/proxmox-sdn-types/src/net.rs b/proxmox-sdn-types/src/net.rs
index 3e523fb12d9b..21822bacab1c 100644
--- a/proxmox-sdn-types/src/net.rs
+++ b/proxmox-sdn-types/src/net.rs
@@ -139,7 +139,7 @@ impl Default for NetSelector {
/// between fabrics on the same node. It contains the [`NetSystemId`] and the [`NetSelector`].
/// e.g.: "1921.6800.1002.00"
#[api]
-#[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Net {
afi: NetAFI,
area: NetArea,
@@ -147,6 +147,8 @@ pub struct Net {
selector: NetSelector,
}
+proxmox_serde::forward_serialize_to_display!(Net);
+proxmox_serde::forward_deserialize_to_from_str!(Net);
impl UpdaterType for Net {
type Updater = Option<Net>;
diff --git a/proxmox-ve-config/src/common/valid.rs b/proxmox-ve-config/src/common/valid.rs
index 1f92ef9bb409..f59ddd0a8806 100644
--- a/proxmox-ve-config/src/common/valid.rs
+++ b/proxmox-ve-config/src/common/valid.rs
@@ -1,5 +1,7 @@
use std::ops::Deref;
+use serde::{Deserialize, Serialize};
+
/// A wrapper type for validatable structs.
///
/// It can only be constructed by implementing the [`Validatable`] type for a struct. Its contents
@@ -8,7 +10,7 @@ use std::ops::Deref;
///
/// If you want to edit the content, this struct has to be unwrapped via [`Valid<T>::into_inner`].
#[repr(transparent)]
-#[derive(Clone, Default, Debug)]
+#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Valid<T>(T);
impl<T> Valid<T> {
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index 10025b3544b9..ac5e88e905a3 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -1,12 +1,20 @@
use std::net::{IpAddr, Ipv4Addr};
+
use tracing;
-use proxmox_frr::ser::{self};
+use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
+use proxmox_frr::ser::ospf::{self, OspfInterface, OspfRouter};
+use proxmox_frr::ser::route_map::{
+ AccessAction, AccessListName, AccessListOrPrefixList, RouteMapEntry, RouteMapMatch,
+ RouteMapMatchInner, RouteMapName, RouteMapSet,
+};
+use proxmox_frr::ser::{
+ self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap,
+};
use proxmox_network_types::ip_address::Cidr;
use proxmox_sdn_types::net::Net;
use crate::common::valid::Valid;
-
use crate::sdn::fabric::section_config::protocol::{
openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties},
ospf::OspfInterfaceProperties,
@@ -17,11 +25,11 @@ use crate::sdn::fabric::{FabricConfig, FabricEntry};
/// Constructs the FRR config from the the passed [`Valid<FabricConfig>`].
///
/// Iterates over the [`FabricConfig`] and constructs all the FRR routers, interfaces, route-maps,
-/// etc. which area all appended to the passed [`FrrConfig`].
+/// etc.
pub fn build_fabric(
current_node: NodeId,
config: Valid<FabricConfig>,
- frr_config: &mut ser::FrrConfig,
+ frr_config: &mut FrrConfig,
) -> Result<(), anyhow::Error> {
let mut routemap_seq = 100;
let mut current_router_id: Option<Ipv4Addr> = None;
@@ -48,7 +56,15 @@ pub fn build_fabric(
.as_ref()
.ok_or_else(|| anyhow::anyhow!("no IPv4 or IPv6 set for node"))?;
let (router_name, router_item) = build_openfabric_router(fabric_id, net.clone())?;
- frr_config.router.insert(router_name, router_item);
+
+ if frr_config
+ .openfabric
+ .router
+ .insert(router_name, router_item)
+ .is_some()
+ {
+ tracing::error!("duplicate OpenFabric router");
+ }
// Create dummy interface for fabric
let (interface, interface_name) = build_openfabric_dummy_interface(
@@ -58,6 +74,7 @@ pub fn build_fabric(
)?;
if frr_config
+ .openfabric
.interfaces
.insert(interface_name, interface)
.is_some()
@@ -79,6 +96,7 @@ pub fn build_fabric(
)?;
if frr_config
+ .openfabric
.interfaces
.insert(interface_name, interface)
.is_some()
@@ -91,70 +109,85 @@ pub fn build_fabric(
let rule = ser::route_map::AccessListRule {
action: ser::route_map::AccessAction::Permit,
network: Cidr::from(ipv4cidr),
+ is_ipv6: false,
seq: None,
};
- let access_list_name = ser::route_map::AccessListName::new(format!(
- "pve_openfabric_{}_ips",
- fabric_id
- ));
- frr_config.access_lists.push(ser::route_map::AccessList {
- name: access_list_name,
- rules: vec![rule],
- });
+ let access_list_name =
+ AccessListName::new(format!("pve_openfabric_{}_ips", fabric_id));
+ frr_config.access_lists.insert(access_list_name, vec![rule]);
}
if let Some(ipv6cidr) = fabric.ip6_prefix() {
let rule = ser::route_map::AccessListRule {
action: ser::route_map::AccessAction::Permit,
network: Cidr::from(ipv6cidr),
+ is_ipv6: true,
seq: None,
};
- let access_list_name = ser::route_map::AccessListName::new(format!(
- "pve_openfabric_{}_ip6s",
- fabric_id
- ));
- frr_config.access_lists.push(ser::route_map::AccessList {
- name: access_list_name,
- rules: vec![rule],
- });
+ let access_list_name =
+ AccessListName::new(format!("pve_openfabric_{}_ip6s", fabric_id));
+ frr_config.access_lists.insert(access_list_name, vec![rule]);
}
if let Some(ipv4) = node.ip() {
// create route-map
- frr_config.routemaps.push(build_openfabric_routemap(
- fabric_id,
- IpAddr::V4(ipv4),
- routemap_seq,
- ));
- routemap_seq += 10;
+ let (routemap_name, routemap_rule) =
+ build_openfabric_routemap(fabric_id, IpAddr::V4(ipv4), routemap_seq);
+
+ if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+ routemap.push(routemap_rule)
+ } else {
+ frr_config
+ .routemaps
+ .insert(routemap_name.clone(), vec![routemap_rule]);
+ }
- let protocol_routemap = ser::route_map::ProtocolRouteMap {
- is_ipv6: false,
- protocol: ser::route_map::ProtocolType::Openfabric,
- routemap_name: ser::route_map::RouteMapName::new(
- "pve_openfabric".to_owned(),
- ),
- };
+ routemap_seq += 10;
- frr_config.protocol_routemaps.insert(protocol_routemap);
+ if let Some(routemap) = frr_config
+ .protocol_routemaps
+ .get_mut(&FrrProtocol::Openfabric)
+ {
+ routemap.v4 = Some(routemap_name);
+ } else {
+ frr_config.protocol_routemaps.insert(
+ FrrProtocol::Openfabric,
+ IpProtocolRouteMap {
+ v4: Some(routemap_name),
+ v6: None,
+ },
+ );
+ }
}
+
if let Some(ipv6) = node.ip6() {
// create route-map
- frr_config.routemaps.push(build_openfabric_routemap(
- fabric_id,
- IpAddr::V6(ipv6),
- routemap_seq,
- ));
- routemap_seq += 10;
+ let (routemap_name, routemap_rule) =
+ build_openfabric_routemap(fabric_id, IpAddr::V6(ipv6), routemap_seq);
+
+ if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+ routemap.push(routemap_rule)
+ } else {
+ frr_config
+ .routemaps
+ .insert(routemap_name.clone(), vec![routemap_rule]);
+ }
- let protocol_routemap = ser::route_map::ProtocolRouteMap {
- is_ipv6: true,
- protocol: ser::route_map::ProtocolType::Openfabric,
- routemap_name: ser::route_map::RouteMapName::new(
- "pve_openfabric6".to_owned(),
- ),
- };
+ routemap_seq += 10;
- frr_config.protocol_routemaps.insert(protocol_routemap);
+ if let Some(routemap) = frr_config
+ .protocol_routemaps
+ .get_mut(&FrrProtocol::Openfabric)
+ {
+ routemap.v6 = Some(routemap_name);
+ } else {
+ frr_config.protocol_routemaps.insert(
+ FrrProtocol::Openfabric,
+ IpProtocolRouteMap {
+ v4: None,
+ v6: Some(routemap_name),
+ },
+ );
+ }
}
}
FabricEntry::Ospf(ospf_entry) => {
@@ -169,14 +202,17 @@ pub fn build_fabric(
let frr_word_area = ser::FrrWord::new(fabric.properties().area.to_string())?;
let frr_area = ser::ospf::Area::new(frr_word_area)?;
- let (router_name, router_item) = build_ospf_router(*router_id)?;
- frr_config.router.insert(router_name, router_item);
+
+ if frr_config.ospf.router.is_none() {
+ frr_config.ospf.router = Some(build_ospf_router(*router_id)?);
+ }
// Add dummy interface
let (interface, interface_name) =
build_ospf_dummy_interface(fabric_id, frr_area.clone())?;
if frr_config
+ .ospf
.interfaces
.insert(interface_name, interface)
.is_some()
@@ -191,11 +227,12 @@ pub fn build_fabric(
build_ospf_interface(frr_area.clone(), interface)?;
if frr_config
+ .ospf
.interfaces
.insert(interface_name, interface)
.is_some()
{
- tracing::warn!("An interface cannot be in multiple openfabric fabrics");
+ tracing::warn!("An interface cannot be in multiple ospf fabrics");
}
}
@@ -207,53 +244,59 @@ pub fn build_fabric(
network: Cidr::from(
fabric.ip_prefix().expect("fabric must have a ipv4 prefix"),
),
+ is_ipv6: false,
seq: None,
};
- frr_config.access_lists.push(ser::route_map::AccessList {
- name: access_list_name,
- rules: vec![rule],
- });
+ frr_config.access_lists.insert(access_list_name, vec![rule]);
- let routemap = build_ospf_dummy_routemap(
+ let (routemap_name, routemap_rule) = build_ospf_dummy_routemap(
fabric_id,
node.ip().expect("node must have an ipv4 address"),
routemap_seq,
)?;
routemap_seq += 10;
- frr_config.routemaps.push(routemap);
- let protocol_routemap = ser::route_map::ProtocolRouteMap {
- is_ipv6: false,
- protocol: ser::route_map::ProtocolType::Ospf,
- routemap_name: ser::route_map::RouteMapName::new("pve_ospf".to_owned()),
- };
+ if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+ routemap.push(routemap_rule)
+ } else {
+ frr_config
+ .routemaps
+ .insert(routemap_name.clone(), vec![routemap_rule]);
+ }
- frr_config.protocol_routemaps.insert(protocol_routemap);
+ if let Some(routemap) = frr_config.protocol_routemaps.get_mut(&FrrProtocol::Ospf) {
+ routemap.v4 = Some(routemap_name);
+ } else {
+ frr_config.protocol_routemaps.insert(
+ FrrProtocol::Ospf,
+ IpProtocolRouteMap {
+ v4: Some(routemap_name),
+ v6: None,
+ },
+ );
+ }
}
}
}
+
Ok(())
}
/// Helper that builds a OSPF router with a the router_id.
-fn build_ospf_router(router_id: Ipv4Addr) -> Result<(ser::RouterName, ser::Router), anyhow::Error> {
- let ospf_router = ser::ospf::OspfRouter { router_id };
- let router_item = ser::Router::Ospf(ospf_router);
- let router_name = ser::RouterName::Ospf(ser::ospf::OspfRouterName);
- Ok((router_name, router_item))
+fn build_ospf_router(router_id: Ipv4Addr) -> Result<OspfRouter, anyhow::Error> {
+ Ok(ser::ospf::OspfRouter { router_id })
}
/// Helper that builds a OpenFabric router from a fabric_id and a [`Net`].
fn build_openfabric_router(
fabric_id: &FabricId,
net: Net,
-) -> Result<(ser::RouterName, ser::Router), anyhow::Error> {
- let ofr = ser::openfabric::OpenfabricRouter { net };
- let router_item = ser::Router::Openfabric(ofr);
- let frr_word_id = ser::FrrWord::new(fabric_id.to_string())?;
- let router_name = ser::RouterName::Openfabric(frr_word_id.into());
+) -> Result<(OpenfabricRouterName, OpenfabricRouter), anyhow::Error> {
+ let router_item = ser::openfabric::OpenfabricRouter { net };
+ let frr_word_id = FrrWord::new(fabric_id.to_string())?;
+ let router_name = frr_word_id.into();
Ok((router_name, router_item))
}
@@ -261,10 +304,10 @@ fn build_openfabric_router(
fn build_ospf_interface(
area: ser::ospf::Area,
interface: &OspfInterfaceProperties,
-) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
+) -> Result<(Interface<OspfInterface>, InterfaceName), anyhow::Error> {
let frr_interface = ser::ospf::OspfInterface {
area,
- // Interfaces are always none-passive
+ // Interfaces are always non-passive
passive: None,
network_type: if interface.ip.is_some() {
None
@@ -273,21 +316,20 @@ fn build_ospf_interface(
},
};
- let interface_name = ser::InterfaceName::Ospf(interface.name.as_str().try_into()?);
+ let interface_name = interface.name.as_str().try_into()?;
Ok((frr_interface.into(), interface_name))
}
/// Helper that builds the OSPF dummy interface using the [`FabricId`] and the [`ospf::Area`].
fn build_ospf_dummy_interface(
fabric_id: &FabricId,
- area: ser::ospf::Area,
-) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
- let frr_interface = ser::ospf::OspfInterface {
- area,
- passive: Some(true),
- network_type: None,
- };
- let interface_name = ser::InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
+ area: ospf::Area,
+) -> Result<(Interface<OspfInterface>, InterfaceName), anyhow::Error> {
+ let frr_interface = ser::ospf::OspfInterface::builder()
+ .area(area)
+ .passive(true)
+ .build();
+ let interface_name = format!("dummy_{}", fabric_id).try_into()?;
Ok((frr_interface.into(), interface_name))
}
@@ -301,8 +343,8 @@ fn build_openfabric_interface(
fabric_config: &OpenfabricProperties,
is_ipv4: bool,
is_ipv6: bool,
-) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
- let frr_word = ser::FrrWord::new(fabric_id.to_string())?;
+) -> Result<(Interface<OpenfabricInterface>, InterfaceName), anyhow::Error> {
+ let frr_word = FrrWord::new(fabric_id.to_string())?;
let mut frr_interface = ser::openfabric::OpenfabricInterface {
fabric_id: frr_word.into(),
// Every interface is not passive by default
@@ -319,7 +361,7 @@ fn build_openfabric_interface(
if frr_interface.hello_interval.is_none() {
frr_interface.hello_interval = fabric_config.hello_interval;
}
- let interface_name = ser::InterfaceName::Openfabric(interface.name.as_str().try_into()?);
+ let interface_name = interface.name.as_str().try_into()?;
Ok((frr_interface.into(), interface_name))
}
@@ -328,18 +370,15 @@ fn build_openfabric_dummy_interface(
fabric_id: &FabricId,
is_ipv4: bool,
is_ipv6: bool,
-) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
- let frr_word = ser::FrrWord::new(fabric_id.to_string())?;
- let frr_interface = ser::openfabric::OpenfabricInterface {
- fabric_id: frr_word.into(),
- hello_interval: None,
- passive: Some(true),
- csnp_interval: None,
- hello_multiplier: None,
- is_ipv4,
- is_ipv6,
- };
- let interface_name = ser::InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
+) -> Result<(Interface<OpenfabricInterface>, InterfaceName), anyhow::Error> {
+ let frr_word = FrrWord::new(fabric_id.to_string())?;
+ let frr_interface = ser::openfabric::OpenfabricInterface::builder()
+ .fabric_id(frr_word.into())
+ .passive(true)
+ .is_ipv4(is_ipv4)
+ .is_ipv6(is_ipv6)
+ .build();
+ let interface_name = format!("dummy_{}", fabric_id).try_into()?;
Ok((frr_interface.into(), interface_name))
}
@@ -348,29 +387,32 @@ fn build_openfabric_routemap(
fabric_id: &FabricId,
router_ip: IpAddr,
seq: u32,
-) -> ser::route_map::RouteMap {
+) -> (RouteMapName, RouteMapEntry) {
let routemap_name = match router_ip {
IpAddr::V4(_) => ser::route_map::RouteMapName::new("pve_openfabric".to_owned()),
IpAddr::V6(_) => ser::route_map::RouteMapName::new("pve_openfabric6".to_owned()),
};
- ser::route_map::RouteMap {
- name: routemap_name.clone(),
- seq,
- action: ser::route_map::AccessAction::Permit,
- matches: vec![match router_ip {
- IpAddr::V4(_) => {
- ser::route_map::RouteMapMatch::V4(ser::route_map::RouteMapMatchInner::IpAddress(
- ser::route_map::AccessListName::new(format!("pve_openfabric_{fabric_id}_ips")),
- ))
- }
- IpAddr::V6(_) => {
- ser::route_map::RouteMapMatch::V6(ser::route_map::RouteMapMatchInner::IpAddress(
- ser::route_map::AccessListName::new(format!("pve_openfabric_{fabric_id}_ip6s")),
- ))
- }
- }],
- sets: vec![ser::route_map::RouteMapSet::IpSrc(router_ip)],
- }
+ (
+ routemap_name,
+ RouteMapEntry {
+ seq,
+ action: ser::route_map::AccessAction::Permit,
+ matches: vec![match router_ip {
+ IpAddr::V4(_) => RouteMapMatch::V4(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::AccessList(AccessListName::new(format!(
+ "pve_openfabric_{fabric_id}_ips"
+ ))),
+ )),
+ IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::AccessList(AccessListName::new(format!(
+ "pve_openfabric_{fabric_id}_ip6s"
+ ))),
+ )),
+ }],
+ sets: vec![RouteMapSet::Src(router_ip)],
+ custom_frr_config: Vec::new(),
+ },
+ )
}
/// Helper that builds a RouteMap for the OSPF protocol.
@@ -378,20 +420,20 @@ fn build_ospf_dummy_routemap(
fabric_id: &FabricId,
router_ip: Ipv4Addr,
seq: u32,
-) -> Result<ser::route_map::RouteMap, anyhow::Error> {
+) -> Result<(RouteMapName, RouteMapEntry), anyhow::Error> {
let routemap_name = ser::route_map::RouteMapName::new("pve_ospf".to_owned());
// create route-map
- let routemap = ser::route_map::RouteMap {
- name: routemap_name.clone(),
+ let routemap = RouteMapEntry {
seq,
- action: ser::route_map::AccessAction::Permit,
- matches: vec![ser::route_map::RouteMapMatch::V4(
- ser::route_map::RouteMapMatchInner::IpAddress(ser::route_map::AccessListName::new(
- format!("pve_ospf_{fabric_id}_ips"),
- )),
- )],
- sets: vec![ser::route_map::RouteMapSet::IpSrc(IpAddr::from(router_ip))],
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::AccessList(AccessListName::new(format!(
+ "pve_ospf_{fabric_id}_ips"
+ ))),
+ ))],
+ sets: vec![RouteMapSet::Src(IpAddr::from(router_ip))],
+ custom_frr_config: Vec::new(),
};
- Ok(routemap)
+ Ok((routemap_name, routemap))
}
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap
index 98eb50415e36..052a3f7e501c 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router openfabric uwu
net 49.0001.1921.6800.2008.00
exit
@@ -31,4 +32,3 @@ route-map pve_openfabric permit 100
exit
!
ip protocol openfabric route-map pve_openfabric
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap
index 4453ac49377f..f456e819098a 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router openfabric uwu
net 49.0001.1921.6800.2009.00
exit
@@ -30,4 +31,3 @@ route-map pve_openfabric permit 100
exit
!
ip protocol openfabric route-map pve_openfabric
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap
index 48ac9092045e..ae81da3a4a00 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap
@@ -1,35 +1,35 @@
---
source: proxmox-ve-config/tests/fabric/main.rs
expression: output
-snapshot_kind: text
---
+!
router openfabric uwu
net 49.0001.1921.6800.2008.00
exit
!
interface dummy_uwu
- ipv6 router openfabric uwu
ip router openfabric uwu
+ ipv6 router openfabric uwu
openfabric passive
exit
!
interface ens19
- ipv6 router openfabric uwu
ip router openfabric uwu
+ ipv6 router openfabric uwu
openfabric hello-interval 4
exit
!
interface ens20
- ipv6 router openfabric uwu
ip router openfabric uwu
+ ipv6 router openfabric uwu
openfabric hello-interval 4
openfabric hello-multiplier 50
exit
!
-access-list pve_openfabric_uwu_ips permit 192.168.2.0/24
-!
ipv6 access-list pve_openfabric_uwu_ip6s permit 2001:db8::/64
!
+access-list pve_openfabric_uwu_ips permit 192.168.2.0/24
+!
route-map pve_openfabric permit 100
match ip address pve_openfabric_uwu_ips
set src 192.168.2.8
@@ -43,4 +43,3 @@ exit
ip protocol openfabric route-map pve_openfabric
!
ipv6 protocol openfabric route-map pve_openfabric6
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap
index d7ab1d7e2a61..21c75e4f5861 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap
@@ -1,8 +1,8 @@
---
source: proxmox-ve-config/tests/fabric/main.rs
expression: output
-snapshot_kind: text
---
+!
router openfabric uwu
net 49.0001.0000.0000.000a.00
exit
@@ -30,5 +30,5 @@ route-map pve_openfabric6 permit 100
set src a:b::a
exit
!
-ipv6 protocol openfabric route-map pve_openfabric6
!
+ipv6 protocol openfabric route-map pve_openfabric6
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap
index ad6c6db8eb8b..5e0c18eb2493 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router openfabric test1
net 49.0001.1921.6800.2009.00
exit
@@ -46,4 +47,3 @@ route-map pve_openfabric permit 110
exit
!
ip protocol openfabric route-map pve_openfabric
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap
index a303f31f3d1a..ee47866edd67 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router ospf
ospf router-id 10.10.10.1
exit
@@ -29,4 +30,3 @@ route-map pve_ospf permit 100
exit
!
ip protocol ospf route-map pve_ospf
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap
index 46c30b22abdf..209f3406757b 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router ospf
ospf router-id 10.10.10.2
exit
@@ -25,4 +26,3 @@ route-map pve_ospf permit 100
exit
!
ip protocol ospf route-map pve_ospf
-!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap
index 1d2a7c3c272d..225d60bf7edb 100644
--- a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap
@@ -3,6 +3,7 @@ source: proxmox-ve-config/tests/fabric/main.rs
expression: output
snapshot_kind: text
---
+!
router ospf
ospf router-id 192.168.1.9
exit
@@ -42,4 +43,3 @@ route-map pve_ospf permit 110
exit
!
ip protocol ospf route-map pve_ospf
-!
--
2.47.3
next prev parent reply other threads:[~2026-02-03 16:05 UTC|newest]
Thread overview: 24+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-03 16:01 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} 00/23] Generate frr config using jinja templates and rust types Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 1/9] ve-config: firewall: cargo fmt Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 2/9] frr: add proxmox-frr-templates package that contains templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 3/9] ve-config: remove FrrConfigBuilder struct Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 4/9] sdn-types: support variable-length NET identifier Gabriel Goller
2026-02-03 16:01 ` Gabriel Goller [this message]
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 6/9] frr: add isis configuration and templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 7/9] frr: support custom frr configuration lines Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 8/9] frr: add bgp support with templates and serialization Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 9/9] frr: store frr template content as a const map Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-perl-rs 1/2] sdn: add function to generate the frr config for all daemons Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-perl-rs 2/2] sdn: add method to get a frr template Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 01/10] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 02/10] sdn: tests: add missing comment " Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 03/10] tests: use Test::Differences to make test assertions Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 04/10] sdn: write structured frr config that can be rendered using templates Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 05/10] tests: rearrange some statements in the frr config Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 06/10] sdn: adjust frr.conf.local merging to rust template types Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 07/10] cli: add pvesdn cli tool for managing frr template overrides Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 08/10] debian: handle user modifications to FRR templates via ucf Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 09/10] api: add dry-run endpoint for sdn apply to preview changes Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 10/10] test: add test for frr.conf.local merging Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-manager 1/1] sdn: add dry-run view for sdn apply Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-docs 1/1] docs: add man page for the `pvesdn` cli Gabriel Goller
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260203160246.353351-6-g.goller@proxmox.com \
--to=g.goller@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.