From: Gabriel Goller <g.goller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ve-rs v7 10/21] frr: add vtysh integration tests for proxmox-frr
Date: Mon, 23 Mar 2026 14:49:00 +0100 [thread overview]
Message-ID: <20260323134934.243110-11-g.goller@proxmox.com> (raw)
In-Reply-To: <20260323134934.243110-1-g.goller@proxmox.com>
Add integration tests that render FrrConfig structs to frr configuration
text and validate them against a real vtysh (frr) instance. The tests use
vtysh's dry-run mode, which checks for syntax errors and limited semantic
errors without requiring the other FRR daemons.
Tests are split across several files covering common topologies:
- BGP/EVPN (leaf/spine iBGP, EVPN-only, IPv6-only, maximal config)
- Multi-protocol combinations (BGP + OSPF + IS-IS + OpenFabric)
- Route maps, ACLs, prefix lists, protocol route maps
- Edge cases (static routes, VRFs with routes, neighbor local-as, etc.)
Since the tests require a local vtysh installation, the `test-with` crate
is used to automatically mark them as `ignored` when the vtysh binary is
not present.
To allow constructing config structs directly in tests, a few changes
are made to the serializer types:
- Make `IpRoute`, `AddressFamilies`, and `Redistribute` fields public
- Expose `create_env()` from `ser/serializer.rs`
- Add `IsisRouterName::new` constructor and `From<FrrWord>` impl
- Add `PrefixListName::new` constructor
- Add `CsnpInterval::new`, `HelloInterval::new`, and
`HelloMultiplier::new` constructors in proxmox-sdn-types, each
validating the given value against the declared API schema constraints
Reviewed-by: Hannes Laimer <h.laimer@proxmox.com>
Tested-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr/Cargo.toml | 3 +
proxmox-frr/src/ser/bgp.rs | 6 +-
proxmox-frr/src/ser/isis.rs | 16 +-
proxmox-frr/src/ser/mod.rs | 8 +-
proxmox-frr/src/ser/route_map.rs | 6 +
proxmox-frr/src/ser/serializer.rs | 2 +-
proxmox-frr/tests/bgp_evpn.rs | 844 +++++++++++++
proxmox-frr/tests/common/mod.rs | 69 ++
proxmox-frr/tests/fabric_ospf_openfabric.rs | 569 +++++++++
proxmox-frr/tests/template_validation.rs | 676 ++++++++++
proxmox-frr/tests/weird_combinations.rs | 1225 +++++++++++++++++++
proxmox-sdn-types/src/openfabric.rs | 32 +-
12 files changed, 3445 insertions(+), 11 deletions(-)
create mode 100644 proxmox-frr/tests/bgp_evpn.rs
create mode 100644 proxmox-frr/tests/common/mod.rs
create mode 100644 proxmox-frr/tests/fabric_ospf_openfabric.rs
create mode 100644 proxmox-frr/tests/template_validation.rs
create mode 100644 proxmox-frr/tests/weird_combinations.rs
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
index 37a112e73870..d10233af29ad 100644
--- a/proxmox-frr/Cargo.toml
+++ b/proxmox-frr/Cargo.toml
@@ -21,3 +21,6 @@ proxmox-network-types = { workspace = true }
proxmox-sdn-types = { workspace = true }
proxmox-serde = { workspace = true }
proxmox-sortable-macro = "1"
+
+[dev-dependencies]
+test-with = { version = "0.12", default-features = false, features = ["executable"] }
diff --git a/proxmox-frr/src/ser/bgp.rs b/proxmox-frr/src/ser/bgp.rs
index 4510ccd8ddd9..6eb7c573cd9c 100644
--- a/proxmox-frr/src/ser/bgp.rs
+++ b/proxmox-frr/src/ser/bgp.rs
@@ -137,9 +137,9 @@ pub struct CommonAddressFamilyOptions {
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Default)]
pub struct AddressFamilies {
- ipv4_unicast: Option<Ipv4UnicastAF>,
- ipv6_unicast: Option<Ipv6UnicastAF>,
- l2vpn_evpn: Option<L2vpnEvpnAF>,
+ pub ipv4_unicast: Option<Ipv4UnicastAF>,
+ pub ipv6_unicast: Option<Ipv6UnicastAF>,
+ pub l2vpn_evpn: Option<L2vpnEvpnAF>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
diff --git a/proxmox-frr/src/ser/isis.rs b/proxmox-frr/src/ser/isis.rs
index 211c5b21e9e1..eb2abc3103e9 100644
--- a/proxmox-frr/src/ser/isis.rs
+++ b/proxmox-frr/src/ser/isis.rs
@@ -9,6 +9,18 @@ use crate::ser::FrrWord;
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct IsisRouterName(FrrWord);
+impl IsisRouterName {
+ pub fn new(name: FrrWord) -> Self {
+ Self(name)
+ }
+}
+
+impl From<FrrWord> for IsisRouterName {
+ fn from(value: FrrWord) -> Self {
+ Self(value)
+ }
+}
+
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum IsisLevel {
#[serde(rename = "level-1")]
@@ -21,8 +33,8 @@ pub enum IsisLevel {
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Redistribute {
- ipv4_connected: IsisLevel,
- ipv6_connected: IsisLevel,
+ pub ipv4_connected: IsisLevel,
+ pub ipv6_connected: IsisLevel,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
index 7bb48364fadb..692fb7fae66c 100644
--- a/proxmox-frr/src/ser/mod.rs
+++ b/proxmox-frr/src/ser/mod.rs
@@ -164,10 +164,10 @@ pub enum IpOrInterface {
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct IpRoute {
#[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
- is_ipv6: bool,
- prefix: Cidr,
- via: IpOrInterface,
- vrf: Option<InterfaceName>,
+ pub is_ipv6: bool,
+ pub prefix: Cidr,
+ pub via: IpOrInterface,
+ pub vrf: Option<InterfaceName>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
diff --git a/proxmox-frr/src/ser/route_map.rs b/proxmox-frr/src/ser/route_map.rs
index d12ae05fc5b9..a79ded7795df 100644
--- a/proxmox-frr/src/ser/route_map.rs
+++ b/proxmox-frr/src/ser/route_map.rs
@@ -39,6 +39,12 @@ pub struct AccessListName(String);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct PrefixListName(String);
+impl PrefixListName {
+ pub fn new(name: String) -> PrefixListName {
+ PrefixListName(name)
+ }
+}
+
impl AccessListName {
pub fn new(name: String) -> AccessListName {
AccessListName(name)
diff --git a/proxmox-frr/src/ser/serializer.rs b/proxmox-frr/src/ser/serializer.rs
index 2ac85d8cf6c5..8664e5fd927b 100644
--- a/proxmox-frr/src/ser/serializer.rs
+++ b/proxmox-frr/src/ser/serializer.rs
@@ -56,7 +56,7 @@ pub static TEMPLATES: [(&str, &str); 12] = sorted!([
),
]);
-fn create_env<'a>() -> Environment<'a> {
+pub fn create_env<'a>() -> Environment<'a> {
let mut env = Environment::new();
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
diff --git a/proxmox-frr/tests/bgp_evpn.rs b/proxmox-frr/tests/bgp_evpn.rs
new file mode 100644
index 000000000000..94ab624bbb0e
--- /dev/null
+++ b/proxmox-frr/tests/bgp_evpn.rs
@@ -0,0 +1,844 @@
+mod common;
+
+use std::collections::BTreeMap;
+
+use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr};
+
+use proxmox_frr::ser::bgp::{
+ AddressFamilies, AddressFamilyNeighbor, BgpRouter, CommonAddressFamilyOptions,
+ DefaultOriginate, Ipv4UnicastAF, Ipv6UnicastAF, L2vpnEvpnAF, NeighborGroup, NeighborRemoteAs,
+ RedistributeProtocol, Redistribution, Vrf,
+};
+use proxmox_frr::ser::route_map::{
+ AccessAction, AccessListOrPrefixList, PrefixListName, PrefixListRule, RouteMapEntry,
+ RouteMapMatch, RouteMapMatchInner, RouteMapName, RouteMapSet,
+};
+use proxmox_frr::ser::{BgpFrrConfig, FrrConfig, IpOrInterface, IpRoute, VrfName};
+
+use common::{dump_and_check, iface, word};
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_evpn_leaf_ibgp() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: Some(1000),
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: Some(true),
+ neighbor_groups: vec![NeighborGroup {
+ name: word("SPINE"),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::Internal,
+ ips: vec!["10.0.0.10".parse().unwrap(), "10.0.0.11".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: None,
+ update_source: Some(iface("lo")),
+ }],
+ address_families: AddressFamilies {
+ ipv4_unicast: None,
+ ipv6_unicast: None,
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "SPINE".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: Some(true),
+ default_originate: vec![],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: Some(true),
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: vec![" bgp log-neighbor-changes".to_string()],
+ },
+ )]),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_evpn_spine_ebgp() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.10".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: Some(false),
+ graceful_restart_notification: Some(false),
+ disable_ebgp_connected_route_check: Some(true),
+ bestpath_as_path_multipath_relax: Some(true),
+ neighbor_groups: vec![
+ NeighborGroup {
+ name: word("LEAF"),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::External,
+ ips: vec![
+ "10.0.0.1".parse().unwrap(),
+ "10.0.0.2".parse().unwrap(),
+ "10.0.0.3".parse().unwrap(),
+ "10.0.0.4".parse().unwrap(),
+ ],
+ interfaces: vec![],
+ ebgp_multihop: Some(2),
+ update_source: Some(iface("lo")),
+ },
+ NeighborGroup {
+ name: word("BORDER"),
+ bfd: false,
+ local_as: None,
+ remote_as: NeighborRemoteAs::Asn(65100),
+ ips: vec!["10.0.0.50".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: Some(5),
+ update_source: None,
+ },
+ ],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![
+ AddressFamilyNeighbor {
+ name: "LEAF".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ },
+ AddressFamilyNeighbor {
+ name: "BORDER".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: Some(RouteMapName::new(
+ "IMPORT_FILTER".to_string(),
+ )),
+ route_map_out: Some(RouteMapName::new(
+ "EXPORT_FILTER".to_string(),
+ )),
+ },
+ ],
+ custom_frr_config: vec![" maximum-paths 4".to_string()],
+ },
+ networks: vec![
+ Ipv4Cidr::new([10, 0, 0, 0], 24).unwrap(),
+ Ipv4Cidr::new([10, 1, 0, 0], 24).unwrap(),
+ ],
+ redistribute: vec![Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ }],
+ }),
+ ipv6_unicast: None,
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "LEAF".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: None,
+ default_originate: vec![DefaultOriginate::Ipv4, DefaultOriginate::Ipv6],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: Some(true),
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ routemaps: BTreeMap::from([
+ (
+ RouteMapName::new("IMPORT_FILTER".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![RouteMapSet::LocalPreference(150)],
+ custom_frr_config: vec![],
+ }],
+ ),
+ (
+ RouteMapName::new("EXPORT_FILTER".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![RouteMapSet::Metric(200)],
+ custom_frr_config: vec![],
+ }],
+ ),
+ ]),
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_evpn_multi_vrf() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrfs: BTreeMap::from([
+ (
+ iface("vrf-tenant1"),
+ Vrf {
+ vni: Some(1000),
+ ip_routes: vec![IpRoute {
+ is_ipv6: false,
+ prefix: proxmox_network_types::Cidr::new_v4([10, 100, 0, 0], 24)
+ .unwrap(),
+ via: IpOrInterface::Ip("10.0.0.254".parse().unwrap()),
+ vrf: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ ),
+ (
+ iface("vrf-tenant2"),
+ Vrf {
+ vni: Some(2000),
+ ip_routes: vec![],
+ custom_frr_config: vec![],
+ },
+ ),
+ ]),
+ vrf_router: BTreeMap::from([
+ (
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![NeighborGroup {
+ name: word("EVPNPEERS"),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::External,
+ ips: vec!["10.0.0.2".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: Some(3),
+ update_source: Some(iface("lo")),
+ }],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "EVPNPEERS".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ networks: vec![Ipv4Cidr::new([10, 0, 0, 0], 24).unwrap()],
+ redistribute: vec![Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ }],
+ }),
+ ipv6_unicast: None,
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "EVPNPEERS".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: Some(true),
+ default_originate: vec![
+ DefaultOriginate::Ipv4,
+ DefaultOriginate::Ipv6,
+ ],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: Some(true),
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: vec![],
+ },
+ ),
+ (
+ VrfName::Custom("cool".to_string()),
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: None,
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![word("vrf-tenant1")],
+ neighbors: vec![],
+ custom_frr_config: vec![],
+ },
+ networks: vec![],
+ redistribute: vec![Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ }],
+ }),
+ ipv6_unicast: None,
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![],
+ custom_frr_config: vec![],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: Some(true),
+ default_originate: vec![DefaultOriginate::Ipv4],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: None,
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: vec![],
+ },
+ ),
+ ]),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_evpn_dual_stack() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.5".parse().unwrap(),
+ coalesce_time: Some(500),
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: Some(false),
+ graceful_restart_notification: Some(false),
+ disable_ebgp_connected_route_check: Some(true),
+ bestpath_as_path_multipath_relax: Some(true),
+ neighbor_groups: vec![
+ NeighborGroup {
+ name: word("FABRIC"),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::External,
+ ips: vec!["10.0.0.1".parse().unwrap(), "10.0.0.2".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: Some(3),
+ update_source: Some(iface("lo")),
+ },
+ NeighborGroup {
+ name: word("FABRICv6"),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::External,
+ ips: vec!["fd00::1".parse().unwrap(), "fd00::2".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: Some(3),
+ update_source: Some(iface("lo")),
+ },
+ ],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "FABRIC".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: Some(RouteMapName::new("V4_IN".to_string())),
+ route_map_out: Some(RouteMapName::new("V4_OUT".to_string())),
+ }],
+ custom_frr_config: vec![],
+ },
+ networks: vec![
+ Ipv4Cidr::new([10, 0, 0, 0], 24).unwrap(),
+ Ipv4Cidr::new([172, 16, 0, 0], 16).unwrap(),
+ ],
+ redistribute: vec![
+ Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ },
+ Redistribution {
+ protocol: RedistributeProtocol::Static,
+ metric: Some(50),
+ route_map: None,
+ },
+ Redistribution {
+ protocol: RedistributeProtocol::Kernel,
+ metric: Some(200),
+ route_map: None,
+ },
+ ],
+ }),
+ ipv6_unicast: Some(Ipv6UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "FABRICv6".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ networks: vec![
+ Ipv6Cidr::new("fd00::".parse::<std::net::Ipv6Addr>().unwrap(), 64)
+ .unwrap(),
+ Ipv6Cidr::new(
+ "2001:db8::".parse::<std::net::Ipv6Addr>().unwrap(),
+ 48,
+ )
+ .unwrap(),
+ ],
+ redistribute: vec![Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ }],
+ }),
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![
+ AddressFamilyNeighbor {
+ name: "FABRIC".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ },
+ AddressFamilyNeighbor {
+ name: "FABRICv6".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ },
+ ],
+ custom_frr_config: vec![],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: Some(true),
+ default_originate: vec![DefaultOriginate::Ipv4, DefaultOriginate::Ipv6],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: Some(true),
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ prefix_lists: BTreeMap::from([(
+ PrefixListName::new("ALLOWED_V4".to_string()),
+ vec![
+ PrefixListRule {
+ action: AccessAction::Permit,
+ network: proxmox_network_types::Cidr::new_v4([10, 0, 0, 0], 8).unwrap(),
+ seq: Some(10),
+ le: Some(24),
+ ge: None,
+ is_ipv6: false,
+ },
+ PrefixListRule {
+ action: AccessAction::Deny,
+ network: proxmox_network_types::Cidr::new_v4([0, 0, 0, 0], 0).unwrap(),
+ seq: Some(100),
+ le: Some(32),
+ ge: None,
+ is_ipv6: false,
+ },
+ ],
+ )]),
+ routemaps: BTreeMap::from([
+ (
+ RouteMapName::new("V4_IN".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::PrefixList(PrefixListName::new(
+ "ALLOWED_V4".to_string(),
+ )),
+ ))],
+ sets: vec![RouteMapSet::LocalPreference(200)],
+ custom_frr_config: vec![],
+ }],
+ ),
+ (
+ RouteMapName::new("V4_OUT".to_string()),
+ vec![
+ RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![RouteMapSet::Metric(100)],
+ custom_frr_config: vec![],
+ },
+ RouteMapEntry {
+ seq: 20,
+ action: AccessAction::Deny,
+ matches: vec![],
+ sets: vec![],
+ custom_frr_config: vec![],
+ },
+ ],
+ ),
+ ]),
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_evpn_unnumbered() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: Some(true),
+ neighbor_groups: vec![NeighborGroup {
+ name: word("UNDERLAY"),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::External,
+ ips: vec![],
+ interfaces: vec![
+ iface("swp1"),
+ iface("swp2"),
+ iface("swp3"),
+ iface("swp4"),
+ ],
+ ebgp_multihop: None,
+ update_source: None,
+ }],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "UNDERLAY".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ networks: vec![],
+ redistribute: vec![Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ }],
+ }),
+ ipv6_unicast: None,
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "UNDERLAY".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: None,
+ default_originate: vec![],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: None,
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_evpn_route_targets() {
+ let route_targets = proxmox_frr::ser::bgp::RouteTargets {
+ import: vec!["65000:1000".parse().unwrap()],
+ export: vec!["65000:2000".parse().unwrap()],
+ both: vec!["65000:3000".parse().unwrap()],
+ };
+
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![NeighborGroup {
+ name: word("PEERS"),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::External,
+ ips: vec!["10.0.0.2".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: Some(2),
+ update_source: Some(iface("lo")),
+ }],
+ address_families: AddressFamilies {
+ ipv4_unicast: None,
+ ipv6_unicast: None,
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "PEERS".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: None,
+ default_originate: vec![],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: None,
+ autort_as: Some(65000),
+ route_targets: Some(route_targets),
+ }),
+ },
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_redistribute_multiple_protocols() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![],
+ custom_frr_config: vec![],
+ },
+ networks: vec![Ipv4Cidr::new([10, 0, 0, 0], 8).unwrap()],
+ redistribute: vec![
+ Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ },
+ Redistribution {
+ protocol: RedistributeProtocol::Static,
+ metric: Some(100),
+ route_map: None,
+ },
+ Redistribution {
+ protocol: RedistributeProtocol::Ospf,
+ metric: Some(150),
+ route_map: Some(RouteMapName::new("OSPF_TO_BGP".to_string())),
+ },
+ Redistribution {
+ protocol: RedistributeProtocol::Kernel,
+ metric: None,
+ route_map: None,
+ },
+ ],
+ }),
+ ipv6_unicast: None,
+ l2vpn_evpn: None,
+ },
+ custom_frr_config: vec![" timers bgp 10 30".to_string()],
+ },
+ )]),
+ ..Default::default()
+ },
+ routemaps: BTreeMap::from([(
+ RouteMapName::new("OSPF_TO_BGP".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![RouteMapSet::LocalPreference(100)],
+ custom_frr_config: vec![],
+ }],
+ )]),
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_view_router() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![],
+ address_families: AddressFamilies::default(),
+ custom_frr_config: vec![],
+ },
+ )]),
+ view_router: BTreeMap::from([(
+ 1,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: None,
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![NeighborGroup {
+ name: word("VIEW_PEERS"),
+ bfd: false,
+ local_as: None,
+ remote_as: NeighborRemoteAs::External,
+ ips: vec!["10.0.0.100".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: None,
+ update_source: None,
+ }],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "VIEW_PEERS".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ networks: vec![],
+ redistribute: vec![],
+ }),
+ ipv6_unicast: None,
+ l2vpn_evpn: None,
+ },
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
diff --git a/proxmox-frr/tests/common/mod.rs b/proxmox-frr/tests/common/mod.rs
new file mode 100644
index 000000000000..51dc456e4ae4
--- /dev/null
+++ b/proxmox-frr/tests/common/mod.rs
@@ -0,0 +1,69 @@
+use std::process::Command;
+
+use proxmox_frr::ser::{FrrWord, InterfaceName};
+
+pub fn word(s: &str) -> FrrWord {
+ FrrWord::new(s).expect("valid FrrWord")
+}
+
+pub fn iface(s: &str) -> InterfaceName {
+ InterfaceName::try_from(s).expect("valid InterfaceName")
+}
+
+pub fn vtysh_check(config: &str, test_name: &str) -> Result<(), String> {
+ let tmpdir_path = std::env::temp_dir().join(format!("frr-test-{test_name}"));
+ std::fs::create_dir_all(&tmpdir_path).expect("create temp dir");
+ std::fs::write(tmpdir_path.join("vtysh.conf"), "").expect("write vtysh.conf");
+
+ let tmpfile = tmpdir_path.join("test.conf");
+ std::fs::write(&tmpfile, config).expect("write test config");
+
+ let output = Command::new("vtysh")
+ .arg("--config_dir")
+ .arg(&tmpdir_path)
+ .arg("-f")
+ .arg(&tmpfile)
+ .arg("-C")
+ .output()
+ .expect("failed to run vtysh");
+
+ let _ = std::fs::remove_dir_all(&tmpdir_path);
+
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let stderr = String::from_utf8_lossy(&output.stderr);
+
+ if output.status.success() {
+ Ok(())
+ } else {
+ Err(format!(
+ "vtysh rejected config (exit code {:?}):\nstdout: {}\nstderr: {}\nconfig:\n{}",
+ output.status.code(),
+ stdout,
+ stderr,
+ config
+ ))
+ }
+}
+
+/// Dumps the config, prints it, and validates it with vtysh.
+pub fn dump_and_check(config: &proxmox_frr::ser::FrrConfig, test_name: &str) {
+ let output = proxmox_frr::ser::serializer::dump(config).expect("render must succeed");
+ eprintln!("{test_name}:\n{output}");
+ vtysh_check(&output, test_name).unwrap_or_else(|e| panic!("vtysh rejected config: {e}"));
+}
+
+// Extracts the function name from the type name of a closure defined inside the calling function.
+#[macro_export]
+macro_rules! function_name {
+ () => {{
+ fn f() {}
+ fn type_name_of<T>(_: T) -> &'static str {
+ std::any::type_name::<T>()
+ }
+ let name = type_name_of(f);
+ match &name[..name.len() - 3].rfind(':') {
+ Some(pos) => &name[pos + 1..name.len() - 3],
+ None => &name[..name.len() - 3],
+ }
+ }};
+}
diff --git a/proxmox-frr/tests/fabric_ospf_openfabric.rs b/proxmox-frr/tests/fabric_ospf_openfabric.rs
new file mode 100644
index 000000000000..a2503ba1b528
--- /dev/null
+++ b/proxmox-frr/tests/fabric_ospf_openfabric.rs
@@ -0,0 +1,569 @@
+mod common;
+
+use std::collections::BTreeMap;
+
+use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr};
+
+use proxmox_frr::ser::isis::{IsisInterface, IsisLevel, IsisRouter, IsisRouterName, Redistribute};
+use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
+use proxmox_frr::ser::ospf::{Area, NetworkType, OspfInterface, OspfRouter};
+use proxmox_frr::ser::{FrrConfig, Interface, IsisFrrConfig, OpenfabricFrrConfig, OspfFrrConfig};
+use proxmox_sdn_types::openfabric::{CsnpInterval, HelloInterval, HelloMultiplier};
+
+use common::{dump_and_check, iface, word};
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_ospf_multi_interface_backbone() {
+ let config = FrrConfig {
+ ospf: OspfFrrConfig {
+ router: Some(OspfRouter::new("10.0.0.1".parse().unwrap())),
+ interfaces: BTreeMap::from([
+ (
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: None,
+ },
+ },
+ ),
+ (
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 1, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: Some(false),
+ network_type: None,
+ },
+ },
+ ),
+ (
+ iface("eth2"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 2, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: Some(true),
+ network_type: Some(NetworkType::Broadcast),
+ },
+ },
+ ),
+ ]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_ospf_multi_area() {
+ let config = FrrConfig {
+ ospf: OspfFrrConfig {
+ router: Some(OspfRouter::new("10.0.0.1".parse().unwrap())),
+ interfaces: BTreeMap::from([
+ (
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: None,
+ },
+ },
+ ),
+ (
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 1, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("1")).unwrap(),
+ passive: None,
+ network_type: None,
+ },
+ },
+ ),
+ (
+ iface("eth2"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 2, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0.0.0.2")).unwrap(),
+ passive: Some(true),
+ network_type: None,
+ },
+ },
+ ),
+ ]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_ospf_unnumbered_ptp() {
+ let config = FrrConfig {
+ ospf: OspfFrrConfig {
+ router: Some(OspfRouter::new("10.0.0.1".parse().unwrap())),
+ interfaces: BTreeMap::from([
+ (
+ iface("lo"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 32).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: Some(true),
+ network_type: None,
+ },
+ },
+ ),
+ (
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 32).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: Some(NetworkType::PointToPoint),
+ },
+ },
+ ),
+ (
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 32).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: Some(NetworkType::PointToPoint),
+ },
+ },
+ ),
+ ]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_openfabric_multi_fabric_with_timers() {
+ let config = FrrConfig {
+ openfabric: OpenfabricFrrConfig {
+ router: BTreeMap::from([
+ (
+ OpenfabricRouterName::new(word("fabric1")),
+ OpenfabricRouter::new("49.0001.1921.6800.1001.00".parse().unwrap()),
+ ),
+ (
+ OpenfabricRouterName::new(word("fabric2")),
+ OpenfabricRouter::new("49.0002.1921.6800.1001.00".parse().unwrap()),
+ ),
+ ]),
+ interfaces: BTreeMap::from([
+ (
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OpenfabricInterface {
+ fabric_id: OpenfabricRouterName::new(word("fabric1")),
+ passive: Some(false),
+ hello_interval: Some(HelloInterval::new(5).unwrap()),
+ csnp_interval: Some(CsnpInterval::new(5).unwrap()),
+ hello_multiplier: Some(HelloMultiplier::new(3).unwrap()),
+ is_ipv4: true,
+ is_ipv6: false,
+ },
+ },
+ ),
+ (
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![],
+ addresses_v6: vec![Ipv6Cidr::new(
+ "fd00::1".parse::<std::net::Ipv6Addr>().unwrap(),
+ 64,
+ )
+ .unwrap()],
+ properties: OpenfabricInterface {
+ fabric_id: OpenfabricRouterName::new(word("fabric2")),
+ passive: None,
+ hello_interval: None,
+ csnp_interval: None,
+ hello_multiplier: None,
+ is_ipv4: false,
+ is_ipv6: true,
+ },
+ },
+ ),
+ (
+ iface("eth2"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 1, 0, 1], 24).unwrap()],
+ addresses_v6: vec![Ipv6Cidr::new(
+ "fd01::1".parse::<std::net::Ipv6Addr>().unwrap(),
+ 64,
+ )
+ .unwrap()],
+ properties: OpenfabricInterface {
+ fabric_id: OpenfabricRouterName::new(word("fabric1")),
+ passive: Some(true),
+ hello_interval: Some(HelloInterval::new(10).unwrap()),
+ csnp_interval: None,
+ hello_multiplier: None,
+ is_ipv4: true,
+ is_ipv6: true,
+ },
+ },
+ ),
+ ]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_openfabric_ipv6_only() {
+ let config = FrrConfig {
+ openfabric: OpenfabricFrrConfig {
+ router: BTreeMap::from([(
+ OpenfabricRouterName::new(word("v6fabric")),
+ OpenfabricRouter::new("49.0001.1921.6800.0001.00".parse().unwrap()),
+ )]),
+ interfaces: BTreeMap::from([
+ (
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![],
+ addresses_v6: vec![
+ Ipv6Cidr::new("fd00::1".parse::<std::net::Ipv6Addr>().unwrap(), 64)
+ .unwrap(),
+ Ipv6Cidr::new("2001:db8::1".parse::<std::net::Ipv6Addr>().unwrap(), 48)
+ .unwrap(),
+ ],
+ properties: OpenfabricInterface {
+ fabric_id: OpenfabricRouterName::new(word("v6fabric")),
+ passive: Some(false),
+ hello_interval: None,
+ csnp_interval: None,
+ hello_multiplier: None,
+ is_ipv4: false,
+ is_ipv6: true,
+ },
+ },
+ ),
+ (
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![],
+ addresses_v6: vec![Ipv6Cidr::new(
+ "fd01::1".parse::<std::net::Ipv6Addr>().unwrap(),
+ 64,
+ )
+ .unwrap()],
+ properties: OpenfabricInterface {
+ fabric_id: OpenfabricRouterName::new(word("v6fabric")),
+ passive: None,
+ hello_interval: Some(HelloInterval::new(2).unwrap()),
+ csnp_interval: Some(CsnpInterval::new(5).unwrap()),
+ hello_multiplier: Some(HelloMultiplier::new(4).unwrap()),
+ is_ipv4: false,
+ is_ipv6: true,
+ },
+ },
+ ),
+ ]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_ospf_plus_openfabric() {
+ let config = FrrConfig {
+ ospf: OspfFrrConfig {
+ router: Some(OspfRouter::new("10.0.0.1".parse().unwrap())),
+ interfaces: BTreeMap::from([
+ (
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: Some(NetworkType::PointToPoint),
+ },
+ },
+ ),
+ (
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 1, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: Some(true),
+ network_type: None,
+ },
+ },
+ ),
+ ]),
+ },
+ openfabric: OpenfabricFrrConfig {
+ router: BTreeMap::from([(
+ OpenfabricRouterName::new(word("myfabric")),
+ OpenfabricRouter::new("49.0001.1000.0000.0001.00".parse().unwrap()),
+ )]),
+ interfaces: BTreeMap::from([
+ (
+ iface("eth2"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 1, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OpenfabricInterface {
+ fabric_id: OpenfabricRouterName::new(word("myfabric")),
+ passive: Some(false),
+ hello_interval: None,
+ csnp_interval: None,
+ hello_multiplier: None,
+ is_ipv4: true,
+ is_ipv6: false,
+ },
+ },
+ ),
+ (
+ iface("eth3"),
+ Interface {
+ addresses_v4: vec![],
+ addresses_v6: vec![Ipv6Cidr::new(
+ "fd00::1".parse::<std::net::Ipv6Addr>().unwrap(),
+ 64,
+ )
+ .unwrap()],
+ properties: OpenfabricInterface {
+ fabric_id: OpenfabricRouterName::new(word("myfabric")),
+ passive: None,
+ hello_interval: Some(HelloInterval::new(5).unwrap()),
+ csnp_interval: None,
+ hello_multiplier: None,
+ is_ipv4: false,
+ is_ipv6: true,
+ },
+ },
+ ),
+ ]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_isis_full_featured() {
+ let config = FrrConfig {
+ isis: IsisFrrConfig {
+ router: BTreeMap::from([(
+ IsisRouterName::new(word("CORE")),
+ IsisRouter {
+ net: "49.0001.1921.6800.1001.00".parse().unwrap(),
+ log_adjacency_changes: Some(true),
+ redistribute: Some(Redistribute {
+ ipv4_connected: IsisLevel::Level1,
+ ipv6_connected: IsisLevel::Level2,
+ }),
+ custom_frr_config: vec![
+ " metric-style wide".to_string(),
+ " is-type level-1-2".to_string(),
+ ],
+ },
+ )]),
+ interfaces: BTreeMap::from([
+ (
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![Ipv6Cidr::new(
+ "fd00::1".parse::<std::net::Ipv6Addr>().unwrap(),
+ 64,
+ )
+ .unwrap()],
+ properties: IsisInterface {
+ domain: IsisRouterName::new(word("CORE")),
+ is_ipv4: true,
+ is_ipv6: true,
+ custom_frr_config: vec![" isis circuit-type level-1-2".to_string()],
+ },
+ },
+ ),
+ (
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 1, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: IsisInterface {
+ domain: IsisRouterName::new(word("CORE")),
+ is_ipv4: true,
+ is_ipv6: false,
+ custom_frr_config: vec![],
+ },
+ },
+ ),
+ ]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_isis_plus_openfabric() {
+ let config = FrrConfig {
+ isis: IsisFrrConfig {
+ router: BTreeMap::from([(
+ IsisRouterName::new(word("isisnet")),
+ IsisRouter {
+ net: "49.0001.1921.6800.1001.00".parse().unwrap(),
+ log_adjacency_changes: Some(true),
+ redistribute: Some(Redistribute {
+ ipv4_connected: IsisLevel::Level1,
+ ipv6_connected: IsisLevel::Level1,
+ }),
+ custom_frr_config: vec![],
+ },
+ )]),
+ interfaces: BTreeMap::from([(
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: IsisInterface {
+ domain: IsisRouterName::new(word("isisnet")),
+ is_ipv4: true,
+ is_ipv6: false,
+ custom_frr_config: vec![],
+ },
+ },
+ )]),
+ },
+ openfabric: OpenfabricFrrConfig {
+ router: BTreeMap::from([(
+ OpenfabricRouterName::new(word("ofnet")),
+ OpenfabricRouter::new("49.0002.1921.6800.1001.00".parse().unwrap()),
+ )]),
+ interfaces: BTreeMap::from([(
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 1, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OpenfabricInterface {
+ fabric_id: OpenfabricRouterName::new(word("ofnet")),
+ passive: Some(false),
+ hello_interval: None,
+ csnp_interval: None,
+ hello_multiplier: None,
+ is_ipv4: true,
+ is_ipv6: false,
+ },
+ },
+ )]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_ospf_all_network_types() {
+ let config = FrrConfig {
+ ospf: OspfFrrConfig {
+ router: Some(OspfRouter::new("10.0.0.1".parse().unwrap())),
+ interfaces: BTreeMap::from([
+ (
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: Some(NetworkType::Broadcast),
+ },
+ },
+ ),
+ (
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 1, 1], 32).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: Some(NetworkType::PointToPoint),
+ },
+ },
+ ),
+ (
+ iface("eth2"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 2, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: Some(NetworkType::NonBroadcast),
+ },
+ },
+ ),
+ (
+ iface("eth3"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 3, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: Some(NetworkType::PointToMultipoint),
+ },
+ },
+ ),
+ ]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
diff --git a/proxmox-frr/tests/template_validation.rs b/proxmox-frr/tests/template_validation.rs
new file mode 100644
index 000000000000..80ed73ecc2e4
--- /dev/null
+++ b/proxmox-frr/tests/template_validation.rs
@@ -0,0 +1,676 @@
+mod common;
+
+use std::collections::BTreeMap;
+
+use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
+
+use proxmox_frr::ser::bgp::{
+ AddressFamilies, AddressFamilyNeighbor, BgpRouter, CommonAddressFamilyOptions,
+ DefaultOriginate, Ipv4UnicastAF, Ipv6UnicastAF, L2vpnEvpnAF, NeighborGroup, NeighborRemoteAs,
+ RedistributeProtocol, Redistribution, Vrf,
+};
+use proxmox_frr::ser::isis::{IsisInterface, IsisLevel, IsisRouter, IsisRouterName, Redistribute};
+use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
+use proxmox_frr::ser::ospf::{Area, NetworkType, OspfInterface, OspfRouter};
+use proxmox_frr::ser::route_map::{
+ AccessAction, AccessListName, AccessListOrPrefixList, AccessListRule, PrefixListName,
+ PrefixListRule, RouteMapEntry, RouteMapMatch, RouteMapMatchInner, RouteMapName, RouteMapSet,
+};
+use proxmox_frr::ser::serializer::{create_env, dump, TEMPLATES};
+use proxmox_frr::ser::{
+ BgpFrrConfig, FrrConfig, FrrProtocol, Interface, IpOrInterface, IpProtocolRouteMap, IpRoute,
+ OpenfabricFrrConfig, OspfFrrConfig, VrfName,
+};
+
+use common::{dump_and_check, iface, word};
+
+#[test]
+fn test_all_templates_load_and_parse() {
+ let env = create_env();
+
+ for (name, _) in TEMPLATES.iter() {
+ let result = env.get_template(name);
+ assert!(
+ result.is_ok(),
+ "Failed to load and parse template '{name}': {:?}",
+ result.err()
+ );
+ }
+}
+
+#[test]
+fn test_empty_config_renders() {
+ let config = FrrConfig::default();
+ let result = dump(&config);
+ assert!(
+ result.is_ok(),
+ "Empty config failed to render: {:?}",
+ result.err()
+ );
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_empty_config() {
+ let config = FrrConfig::default();
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_simple() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![],
+ address_families: AddressFamilies::default(),
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_with_neighbors_and_address_families() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: Some(1000),
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: Some(false),
+ graceful_restart_notification: Some(false),
+ disable_ebgp_connected_route_check: Some(true),
+ bestpath_as_path_multipath_relax: Some(true),
+ neighbor_groups: vec![NeighborGroup {
+ name: word("PEERS"),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::External,
+ ips: vec!["10.0.0.2".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: Some(2),
+ update_source: Some(iface("lo")),
+ }],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "PEERS".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ networks: vec![Ipv4Cidr::new([10, 0, 0, 0], 24).unwrap()],
+ redistribute: vec![Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ }],
+ }),
+ ipv6_unicast: Some(Ipv6UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "PEERS".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ networks: vec![Ipv6Cidr::new(
+ "fd00::".parse::<std::net::Ipv6Addr>().unwrap(),
+ 64,
+ )
+ .unwrap()],
+ redistribute: vec![Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ }],
+ }),
+ l2vpn_evpn: None,
+ },
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_with_vrf() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrfs: BTreeMap::from([(
+ iface("vrf-test"),
+ Vrf {
+ vni: Some(100),
+ ip_routes: vec![],
+ custom_frr_config: vec![],
+ },
+ )]),
+ vrf_router: BTreeMap::from([
+ (
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: None,
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![],
+ address_families: AddressFamilies::default(),
+ custom_frr_config: vec![],
+ },
+ ),
+ (
+ VrfName::Custom("vrf-test".to_string()),
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: None,
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![],
+ address_families: AddressFamilies {
+ ipv4_unicast: None,
+ ipv6_unicast: None,
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![],
+ custom_frr_config: vec![],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: Some(true),
+ default_originate: vec![
+ DefaultOriginate::Ipv4,
+ DefaultOriginate::Ipv6,
+ ],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: Some(true),
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: vec![],
+ },
+ ),
+ ]),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_ospf() {
+ let config = FrrConfig {
+ ospf: OspfFrrConfig {
+ router: Some(OspfRouter::new("10.0.0.1".parse().unwrap())),
+ interfaces: BTreeMap::from([(
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: Some(true),
+ network_type: Some(NetworkType::PointToPoint),
+ },
+ },
+ )]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_openfabric() {
+ let config = FrrConfig {
+ openfabric: OpenfabricFrrConfig {
+ router: BTreeMap::from([(
+ OpenfabricRouterName::new(word("fabric1")),
+ OpenfabricRouter::new("49.0001.1921.6800.1001.00".parse().unwrap()),
+ )]),
+ interfaces: BTreeMap::from([(
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OpenfabricInterface {
+ fabric_id: OpenfabricRouterName::new(word("fabric1")),
+ passive: Some(false),
+ hello_interval: None,
+ csnp_interval: None,
+ hello_multiplier: None,
+ is_ipv4: true,
+ is_ipv6: false,
+ },
+ },
+ )]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_isis() {
+ let config = FrrConfig {
+ isis: proxmox_frr::ser::IsisFrrConfig {
+ router: BTreeMap::from([(
+ IsisRouterName::new(word("myisis")),
+ IsisRouter {
+ net: "49.0001.1921.6800.1001.00".parse().unwrap(),
+ log_adjacency_changes: Some(true),
+ redistribute: Some(Redistribute {
+ ipv4_connected: IsisLevel::Level1,
+ ipv6_connected: IsisLevel::Level1,
+ }),
+ custom_frr_config: vec![],
+ },
+ )]),
+ interfaces: BTreeMap::from([(
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: IsisInterface {
+ domain: IsisRouterName::new(word("myisis")),
+ is_ipv4: true,
+ is_ipv6: false,
+ custom_frr_config: vec![],
+ },
+ },
+ )]),
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_access_lists_and_prefix_lists() {
+ let config = FrrConfig {
+ access_lists: BTreeMap::from([(
+ AccessListName::new("MYACL".to_string()),
+ vec![
+ AccessListRule {
+ action: AccessAction::Permit,
+ network: Cidr::new_v4([10, 0, 0, 0], 24).unwrap(),
+ seq: Some(10),
+ is_ipv6: false,
+ },
+ AccessListRule {
+ action: AccessAction::Deny,
+ network: Cidr::new_v4([192, 168, 0, 0], 16).unwrap(),
+ seq: Some(20),
+ is_ipv6: false,
+ },
+ ],
+ )]),
+ prefix_lists: BTreeMap::from([(
+ PrefixListName::new("MYPFX".to_string()),
+ vec![PrefixListRule {
+ action: AccessAction::Permit,
+ network: Cidr::new_v4([10, 0, 0, 0], 8).unwrap(),
+ seq: Some(10),
+ le: Some(24),
+ ge: Some(16),
+ is_ipv6: false,
+ }],
+ )]),
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_route_maps() {
+ let config = FrrConfig {
+ prefix_lists: BTreeMap::from([(
+ PrefixListName::new("MYPFX".to_string()),
+ vec![PrefixListRule {
+ action: AccessAction::Permit,
+ network: Cidr::new_v4([10, 0, 0, 0], 8).unwrap(),
+ seq: Some(10),
+ le: Some(24),
+ ge: None,
+ is_ipv6: false,
+ }],
+ )]),
+ routemaps: BTreeMap::from([(
+ RouteMapName::new("MYMAP".to_string()),
+ vec![
+ RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::PrefixList(PrefixListName::new(
+ "MYPFX".to_string(),
+ )),
+ ))],
+ sets: vec![RouteMapSet::LocalPreference(200), RouteMapSet::Metric(100)],
+ custom_frr_config: vec![],
+ },
+ RouteMapEntry {
+ seq: 20,
+ action: AccessAction::Deny,
+ matches: vec![],
+ sets: vec![],
+ custom_frr_config: vec![],
+ },
+ ],
+ )]),
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_ip_routes() {
+ let config = FrrConfig {
+ ip_routes: vec![
+ IpRoute {
+ is_ipv6: false,
+ prefix: Cidr::new_v4([10, 10, 0, 0], 24).unwrap(),
+ via: IpOrInterface::Ip("10.0.0.1".parse().unwrap()),
+ vrf: None,
+ },
+ IpRoute {
+ is_ipv6: true,
+ prefix: Cidr::new_v6("fd00:1::".parse::<std::net::Ipv6Addr>().unwrap(), 64)
+ .unwrap(),
+ via: IpOrInterface::Ip("fd00::1".parse().unwrap()),
+ vrf: None,
+ },
+ ],
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_protocol_routemaps() {
+ let config = FrrConfig {
+ routemaps: BTreeMap::from([(
+ RouteMapName::new("OSPF_MAP".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![],
+ custom_frr_config: vec![],
+ }],
+ )]),
+ protocol_routemaps: BTreeMap::from([(
+ FrrProtocol::Ospf,
+ IpProtocolRouteMap {
+ v4: Some(RouteMapName::new("OSPF_MAP".to_string())),
+ v6: None,
+ },
+ )]),
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+fn make_evpn_bgp_router(
+ neighbor_group_name: &str,
+ route_map_in: Option<RouteMapName>,
+ route_map_out: Option<RouteMapName>,
+) -> BgpRouter {
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: Some(true),
+ neighbor_groups: vec![NeighborGroup {
+ name: word(neighbor_group_name),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::External,
+ ips: vec!["10.0.0.2".parse().unwrap(), "10.0.0.3".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: Some(3),
+ update_source: Some(iface("lo")),
+ }],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: neighbor_group_name.to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in,
+ route_map_out,
+ }],
+ custom_frr_config: vec![],
+ },
+ networks: vec![Ipv4Cidr::new([10, 0, 0, 0], 24).unwrap()],
+ redistribute: vec![
+ Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ },
+ Redistribution {
+ protocol: RedistributeProtocol::Static,
+ metric: Some(100),
+ route_map: None,
+ },
+ ],
+ }),
+ ipv6_unicast: None,
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: neighbor_group_name.to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: None,
+ default_originate: vec![],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: None,
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: vec![],
+ }
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_full_combined_config() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([
+ (
+ VrfName::Default,
+ make_evpn_bgp_router(
+ "EVPN",
+ Some(RouteMapName::new("INBOUND".to_string())),
+ Some(RouteMapName::new("OUTBOUND".to_string())),
+ ),
+ ),
+ (
+ VrfName::Custom("cool".to_string()),
+ make_evpn_bgp_router(
+ "EVPN",
+ Some(RouteMapName::new("INBOUND".to_string())),
+ Some(RouteMapName::new("OUTBOUND".to_string())),
+ ),
+ ),
+ ]),
+ ..Default::default()
+ },
+ ospf: OspfFrrConfig {
+ router: Some(OspfRouter::new("10.0.0.1".parse().unwrap())),
+ interfaces: BTreeMap::from([(
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 1, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: None,
+ },
+ },
+ )]),
+ },
+ openfabric: OpenfabricFrrConfig {
+ router: BTreeMap::from([(
+ OpenfabricRouterName::new(word("uwu")),
+ OpenfabricRouter::new("49.0001.1000.0000.1001.00".parse().unwrap()),
+ )]),
+ interfaces: BTreeMap::from([(
+ iface("eth2"),
+ Interface {
+ addresses_v4: vec![],
+ addresses_v6: vec![Ipv6Cidr::new(
+ "fd00::1".parse::<std::net::Ipv6Addr>().unwrap(),
+ 64,
+ )
+ .unwrap()],
+ properties: OpenfabricInterface {
+ fabric_id: OpenfabricRouterName::new(word("uwu")),
+ passive: None,
+ hello_interval: None,
+ csnp_interval: None,
+ hello_multiplier: None,
+ is_ipv4: false,
+ is_ipv6: true,
+ },
+ },
+ )]),
+ },
+ access_lists: BTreeMap::from([(
+ AccessListName::new("MYACL".to_string()),
+ vec![AccessListRule {
+ action: AccessAction::Permit,
+ network: Cidr::new_v4([10, 0, 0, 0], 8).unwrap(),
+ seq: Some(10),
+ is_ipv6: false,
+ }],
+ )]),
+ prefix_lists: BTreeMap::from([(
+ PrefixListName::new("MYPFX".to_string()),
+ vec![PrefixListRule {
+ action: AccessAction::Permit,
+ network: Cidr::new_v4([10, 0, 0, 0], 8).unwrap(),
+ seq: Some(10),
+ le: Some(24),
+ ge: None,
+ is_ipv6: false,
+ }],
+ )]),
+ routemaps: BTreeMap::from([
+ (
+ RouteMapName::new("INBOUND".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::PrefixList(PrefixListName::new(
+ "MYPFX".to_string(),
+ )),
+ ))],
+ sets: vec![RouteMapSet::LocalPreference(200)],
+ custom_frr_config: vec![],
+ }],
+ ),
+ (
+ RouteMapName::new("OUTBOUND".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![RouteMapSet::Metric(50)],
+ custom_frr_config: vec![],
+ }],
+ ),
+ ]),
+ ip_routes: vec![IpRoute {
+ is_ipv6: false,
+ prefix: Cidr::new_v4([172, 16, 0, 0], 16).unwrap(),
+ via: IpOrInterface::Ip("10.0.0.254".parse().unwrap()),
+ vrf: None,
+ }],
+ isis: proxmox_frr::ser::IsisFrrConfig::default(),
+ protocol_routemaps: BTreeMap::from([(
+ FrrProtocol::Ospf,
+ IpProtocolRouteMap {
+ v4: Some(RouteMapName::new("INBOUND".to_string())),
+ v6: None,
+ },
+ )]),
+ custom_frr_config: vec![],
+ };
+
+ dump_and_check(&config, function_name!());
+}
diff --git a/proxmox-frr/tests/weird_combinations.rs b/proxmox-frr/tests/weird_combinations.rs
new file mode 100644
index 000000000000..867a72dac553
--- /dev/null
+++ b/proxmox-frr/tests/weird_combinations.rs
@@ -0,0 +1,1225 @@
+mod common;
+
+use std::collections::BTreeMap;
+
+use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr};
+
+use proxmox_frr::ser::bgp::{
+ AddressFamilies, AddressFamilyNeighbor, BgpRouter, CommonAddressFamilyOptions,
+ DefaultOriginate, Ipv4UnicastAF, Ipv6UnicastAF, L2vpnEvpnAF, NeighborGroup, NeighborRemoteAs,
+ RedistributeProtocol, Redistribution, Vrf,
+};
+use proxmox_frr::ser::isis::{IsisInterface, IsisLevel, IsisRouter, IsisRouterName, Redistribute};
+use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
+use proxmox_frr::ser::ospf::{Area, NetworkType, OspfInterface, OspfRouter};
+use proxmox_frr::ser::route_map::{
+ AccessAction, AccessListName, AccessListOrPrefixList, AccessListRule, PrefixListName,
+ PrefixListRule, RouteMapEntry, RouteMapMatch, RouteMapMatchInner, RouteMapName, RouteMapSet,
+};
+use proxmox_frr::ser::{
+ BgpFrrConfig, FrrConfig, FrrProtocol, Interface, IpOrInterface, IpProtocolRouteMap, IpRoute,
+ IsisFrrConfig, OpenfabricFrrConfig, OspfFrrConfig, VrfName,
+};
+
+use common::{dump_and_check, iface, word};
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_all_protocols_stacked() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: Some(true),
+ neighbor_groups: vec![NeighborGroup {
+ name: word("PEERS"),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::External,
+ ips: vec!["10.0.0.2".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: Some(2),
+ update_source: Some(iface("lo")),
+ }],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "PEERS".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ networks: vec![Ipv4Cidr::new([10, 0, 0, 0], 24).unwrap()],
+ redistribute: vec![
+ Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ },
+ Redistribution {
+ protocol: RedistributeProtocol::Ospf,
+ metric: Some(100),
+ route_map: None,
+ },
+ ],
+ }),
+ ipv6_unicast: None,
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "PEERS".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: None,
+ default_originate: vec![],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: None,
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ ospf: OspfFrrConfig {
+ router: Some(OspfRouter::new("10.0.0.1".parse().unwrap())),
+ interfaces: BTreeMap::from([(
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 1, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: Some(NetworkType::PointToPoint),
+ },
+ },
+ )]),
+ },
+ openfabric: OpenfabricFrrConfig {
+ router: BTreeMap::from([(
+ OpenfabricRouterName::new(word("fab1")),
+ OpenfabricRouter::new("49.0001.1000.0000.0001.00".parse().unwrap()),
+ )]),
+ interfaces: BTreeMap::from([(
+ iface("eth2"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 2, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OpenfabricInterface {
+ fabric_id: OpenfabricRouterName::new(word("fab1")),
+ passive: None,
+ hello_interval: None,
+ csnp_interval: None,
+ hello_multiplier: None,
+ is_ipv4: true,
+ is_ipv6: false,
+ },
+ },
+ )]),
+ },
+ isis: IsisFrrConfig {
+ router: BTreeMap::from([(
+ IsisRouterName::new(word("isis1")),
+ IsisRouter {
+ net: "49.0001.1921.6800.1001.00".parse().unwrap(),
+ log_adjacency_changes: Some(true),
+ redistribute: Some(Redistribute {
+ ipv4_connected: IsisLevel::Level1,
+ ipv6_connected: IsisLevel::Level1,
+ }),
+ custom_frr_config: vec![],
+ },
+ )]),
+ interfaces: BTreeMap::from([(
+ iface("eth3"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 3, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: IsisInterface {
+ domain: IsisRouterName::new(word("isis1")),
+ is_ipv4: true,
+ is_ipv6: false,
+ custom_frr_config: vec![],
+ },
+ },
+ )]),
+ },
+ ip_routes: vec![IpRoute {
+ is_ipv6: false,
+ prefix: proxmox_network_types::Cidr::new_v4([172, 16, 0, 0], 16).unwrap(),
+ via: IpOrInterface::Ip("10.0.0.254".parse().unwrap()),
+ vrf: None,
+ }],
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_custom_frr_config_everywhere() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrfs: BTreeMap::from([(
+ iface("vrf-custom"),
+ Vrf {
+ vni: Some(500),
+ ip_routes: vec![],
+ custom_frr_config: vec![],
+ },
+ )]),
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![],
+ custom_frr_config: vec![
+ " maximum-paths 8".to_string(),
+ " maximum-paths ibgp 4".to_string(),
+ ],
+ },
+ networks: vec![Ipv4Cidr::new([10, 0, 0, 0], 24).unwrap()],
+ redistribute: vec![Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ }],
+ }),
+ ipv6_unicast: None,
+ l2vpn_evpn: None,
+ },
+ custom_frr_config: vec![
+ " bgp log-neighbor-changes".to_string(),
+ " timers bgp 10 30".to_string(),
+ ],
+ },
+ )]),
+ ..Default::default()
+ },
+ isis: IsisFrrConfig {
+ router: BTreeMap::from([(
+ IsisRouterName::new(word("customisis")),
+ IsisRouter {
+ net: "49.0001.1921.6800.1001.00".parse().unwrap(),
+ log_adjacency_changes: None,
+ redistribute: Some(Redistribute {
+ ipv4_connected: IsisLevel::Level1,
+ ipv6_connected: IsisLevel::Level1,
+ }),
+ custom_frr_config: vec![
+ " metric-style wide".to_string(),
+ " is-type level-1".to_string(),
+ ],
+ },
+ )]),
+ interfaces: BTreeMap::from([(
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: IsisInterface {
+ domain: IsisRouterName::new(word("customisis")),
+ is_ipv4: true,
+ is_ipv6: false,
+ custom_frr_config: vec![
+ " isis circuit-type level-1".to_string(),
+ " isis metric 100".to_string(),
+ ],
+ },
+ },
+ )]),
+ },
+ routemaps: BTreeMap::from([(
+ RouteMapName::new("CUSTOMMAP".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![RouteMapSet::Metric(42)],
+ custom_frr_config: vec![" on-match next".to_string()],
+ }],
+ )]),
+ custom_frr_config: vec![
+ "log syslog informational".to_string(),
+ "hostname test-router".to_string(),
+ "service integrated-vtysh-config".to_string(),
+ ],
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_evpn_only_no_unicast() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![NeighborGroup {
+ name: word("EVPN"),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::External,
+ ips: vec!["10.0.0.2".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: Some(3),
+ update_source: Some(iface("lo")),
+ }],
+ address_families: AddressFamilies {
+ ipv4_unicast: None,
+ ipv6_unicast: None,
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "EVPN".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: Some(true),
+ default_originate: vec![DefaultOriginate::Ipv4, DefaultOriginate::Ipv6],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: Some(true),
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_ipv6_only() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![NeighborGroup {
+ name: word("V6PEERS"),
+ bfd: false,
+ local_as: None,
+ remote_as: NeighborRemoteAs::Internal,
+ ips: vec!["fd00::2".parse().unwrap(), "fd00::3".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: None,
+ update_source: Some(iface("lo")),
+ }],
+ address_families: AddressFamilies {
+ ipv4_unicast: None,
+ ipv6_unicast: Some(Ipv6UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "V6PEERS".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![" maximum-paths 4".to_string()],
+ },
+ networks: vec![
+ Ipv6Cidr::new("fd00::".parse::<std::net::Ipv6Addr>().unwrap(), 64)
+ .unwrap(),
+ Ipv6Cidr::new(
+ "2001:db8::".parse::<std::net::Ipv6Addr>().unwrap(),
+ 48,
+ )
+ .unwrap(),
+ ],
+ redistribute: vec![Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ }],
+ }),
+ l2vpn_evpn: None,
+ },
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_only_routemaps_and_acls() {
+ let config = FrrConfig {
+ access_lists: BTreeMap::from([
+ (
+ AccessListName::new("ACL1".to_string()),
+ vec![
+ AccessListRule {
+ action: AccessAction::Permit,
+ network: proxmox_network_types::Cidr::new_v4([10, 0, 0, 0], 8).unwrap(),
+ seq: Some(10),
+ is_ipv6: false,
+ },
+ AccessListRule {
+ action: AccessAction::Deny,
+ network: proxmox_network_types::Cidr::new_v4([0, 0, 0, 0], 0).unwrap(),
+ seq: Some(20),
+ is_ipv6: false,
+ },
+ ],
+ ),
+ (
+ AccessListName::new("ACL_V6".to_string()),
+ vec![AccessListRule {
+ action: AccessAction::Permit,
+ network: proxmox_network_types::Cidr::new_v6(
+ "fd00::".parse::<std::net::Ipv6Addr>().unwrap(),
+ 64,
+ )
+ .unwrap(),
+ seq: Some(10),
+ is_ipv6: true,
+ }],
+ ),
+ ]),
+ prefix_lists: BTreeMap::from([
+ (
+ PrefixListName::new("PFX1".to_string()),
+ vec![
+ PrefixListRule {
+ action: AccessAction::Permit,
+ network: proxmox_network_types::Cidr::new_v4([10, 0, 0, 0], 8).unwrap(),
+ seq: Some(5),
+ le: Some(24),
+ ge: Some(16),
+ is_ipv6: false,
+ },
+ PrefixListRule {
+ action: AccessAction::Deny,
+ network: proxmox_network_types::Cidr::new_v4([0, 0, 0, 0], 0).unwrap(),
+ seq: Some(100),
+ le: Some(32),
+ ge: None,
+ is_ipv6: false,
+ },
+ ],
+ ),
+ (
+ PrefixListName::new("PFX_V6".to_string()),
+ vec![PrefixListRule {
+ action: AccessAction::Permit,
+ network: proxmox_network_types::Cidr::new_v6(
+ "2001:db8::".parse::<std::net::Ipv6Addr>().unwrap(),
+ 32,
+ )
+ .unwrap(),
+ seq: Some(10),
+ le: Some(48),
+ ge: None,
+ is_ipv6: true,
+ }],
+ ),
+ ]),
+ routemaps: BTreeMap::from([
+ (
+ RouteMapName::new("MAP_V4".to_string()),
+ vec![
+ RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::PrefixList(PrefixListName::new(
+ "PFX1".to_string(),
+ )),
+ ))],
+ sets: vec![RouteMapSet::LocalPreference(200), RouteMapSet::Metric(50)],
+ custom_frr_config: vec![],
+ },
+ RouteMapEntry {
+ seq: 20,
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::AccessList(AccessListName::new(
+ "ACL1".to_string(),
+ )),
+ ))],
+ sets: vec![RouteMapSet::Metric(100)],
+ custom_frr_config: vec![],
+ },
+ RouteMapEntry {
+ seq: 100,
+ action: AccessAction::Deny,
+ matches: vec![],
+ sets: vec![],
+ custom_frr_config: vec![],
+ },
+ ],
+ ),
+ (
+ RouteMapName::new("MAP_V6".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V6(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::PrefixList(PrefixListName::new(
+ "PFX_V6".to_string(),
+ )),
+ ))],
+ sets: vec![RouteMapSet::LocalPreference(300)],
+ custom_frr_config: vec![],
+ }],
+ ),
+ ]),
+ custom_frr_config: vec!["hostname acl-only-router".to_string()],
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_many_static_routes() {
+ let config = FrrConfig {
+ ip_routes: vec![
+ IpRoute {
+ is_ipv6: false,
+ prefix: proxmox_network_types::Cidr::new_v4([10, 10, 0, 0], 24).unwrap(),
+ via: IpOrInterface::Ip("10.0.0.1".parse().unwrap()),
+ vrf: None,
+ },
+ IpRoute {
+ is_ipv6: false,
+ prefix: proxmox_network_types::Cidr::new_v4([10, 20, 0, 0], 16).unwrap(),
+ via: IpOrInterface::Ip("10.0.0.2".parse().unwrap()),
+ vrf: None,
+ },
+ IpRoute {
+ is_ipv6: false,
+ prefix: proxmox_network_types::Cidr::new_v4([192, 168, 0, 0], 16).unwrap(),
+ via: IpOrInterface::Interface(iface("eth0")),
+ vrf: None,
+ },
+ IpRoute {
+ is_ipv6: true,
+ prefix: proxmox_network_types::Cidr::new_v6(
+ "fd00:1::".parse::<std::net::Ipv6Addr>().unwrap(),
+ 64,
+ )
+ .unwrap(),
+ via: IpOrInterface::Ip("fd00::1".parse().unwrap()),
+ vrf: None,
+ },
+ IpRoute {
+ is_ipv6: true,
+ prefix: proxmox_network_types::Cidr::new_v6(
+ "2001:db8:1::".parse::<std::net::Ipv6Addr>().unwrap(),
+ 48,
+ )
+ .unwrap(),
+ via: IpOrInterface::Ip("fd00::2".parse().unwrap()),
+ vrf: None,
+ },
+ ],
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_border_gateway_combo() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: Some(true),
+ bestpath_as_path_multipath_relax: Some(true),
+ neighbor_groups: vec![
+ NeighborGroup {
+ name: word("UPSTREAM"),
+ bfd: true,
+ local_as: None,
+ remote_as: NeighborRemoteAs::Asn(64999),
+ ips: vec!["10.0.0.100".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: Some(2),
+ update_source: None,
+ },
+ NeighborGroup {
+ name: word("INTERNAL"),
+ bfd: false,
+ local_as: None,
+ remote_as: NeighborRemoteAs::Internal,
+ ips: vec!["10.0.0.2".parse().unwrap(), "10.0.0.3".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: None,
+ update_source: Some(iface("lo")),
+ },
+ ],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![
+ AddressFamilyNeighbor {
+ name: "UPSTREAM".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: Some(RouteMapName::new(
+ "FROM_UPSTREAM".to_string(),
+ )),
+ route_map_out: Some(RouteMapName::new(
+ "TO_UPSTREAM".to_string(),
+ )),
+ },
+ AddressFamilyNeighbor {
+ name: "INTERNAL".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ },
+ ],
+ custom_frr_config: vec![],
+ },
+ networks: vec![Ipv4Cidr::new([10, 0, 0, 0], 8).unwrap()],
+ redistribute: vec![
+ Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ },
+ Redistribution {
+ protocol: RedistributeProtocol::Ospf,
+ metric: Some(150),
+ route_map: Some(RouteMapName::new("OSPF_INTO_BGP".to_string())),
+ },
+ Redistribution {
+ protocol: RedistributeProtocol::Static,
+ metric: Some(200),
+ route_map: None,
+ },
+ ],
+ }),
+ ipv6_unicast: None,
+ l2vpn_evpn: None,
+ },
+ custom_frr_config: vec![" bgp log-neighbor-changes".to_string()],
+ },
+ )]),
+ ..Default::default()
+ },
+ ospf: OspfFrrConfig {
+ router: Some(OspfRouter::new("10.0.0.1".parse().unwrap())),
+ interfaces: BTreeMap::from([
+ (
+ iface("eth0"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 0, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("0")).unwrap(),
+ passive: None,
+ network_type: None,
+ },
+ },
+ ),
+ (
+ iface("eth1"),
+ Interface {
+ addresses_v4: vec![Ipv4Cidr::new([10, 1, 0, 1], 24).unwrap()],
+ addresses_v6: vec![],
+ properties: OspfInterface {
+ area: Area::new(word("1")).unwrap(),
+ passive: Some(true),
+ network_type: None,
+ },
+ },
+ ),
+ ]),
+ },
+ prefix_lists: BTreeMap::from([(
+ PrefixListName::new("UPSTREAM_NETS".to_string()),
+ vec![PrefixListRule {
+ action: AccessAction::Permit,
+ network: proxmox_network_types::Cidr::new_v4([0, 0, 0, 0], 0).unwrap(),
+ seq: Some(10),
+ le: Some(24),
+ ge: None,
+ is_ipv6: false,
+ }],
+ )]),
+ routemaps: BTreeMap::from([
+ (
+ RouteMapName::new("FROM_UPSTREAM".to_string()),
+ vec![
+ RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V4(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::PrefixList(PrefixListName::new(
+ "UPSTREAM_NETS".to_string(),
+ )),
+ ))],
+ sets: vec![RouteMapSet::LocalPreference(200)],
+ custom_frr_config: vec![],
+ },
+ RouteMapEntry {
+ seq: 100,
+ action: AccessAction::Deny,
+ matches: vec![],
+ sets: vec![],
+ custom_frr_config: vec![],
+ },
+ ],
+ ),
+ (
+ RouteMapName::new("TO_UPSTREAM".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![
+ RouteMapSet::Metric(100),
+ RouteMapSet::Community("65000:100".to_string()),
+ ],
+ custom_frr_config: vec![],
+ }],
+ ),
+ (
+ RouteMapName::new("OSPF_INTO_BGP".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![RouteMapSet::LocalPreference(50)],
+ custom_frr_config: vec![],
+ }],
+ ),
+ ]),
+ ip_routes: vec![IpRoute {
+ is_ipv6: false,
+ prefix: proxmox_network_types::Cidr::new_v4([0, 0, 0, 0], 0).unwrap(),
+ via: IpOrInterface::Ip("10.0.0.100".parse().unwrap()),
+ vrf: None,
+ }],
+ protocol_routemaps: BTreeMap::from([
+ (
+ FrrProtocol::Ospf,
+ IpProtocolRouteMap {
+ v4: Some(RouteMapName::new("OSPF_INTO_BGP".to_string())),
+ v6: None,
+ },
+ ),
+ (
+ FrrProtocol::Bgp,
+ IpProtocolRouteMap {
+ v4: Some(RouteMapName::new("FROM_UPSTREAM".to_string())),
+ v6: None,
+ },
+ ),
+ ]),
+ custom_frr_config: vec!["log syslog informational".to_string()],
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_neighbor_local_as() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![NeighborGroup {
+ name: word("MIGRATION"),
+ bfd: false,
+ local_as: Some(65100),
+ remote_as: NeighborRemoteAs::Asn(65200),
+ ips: vec!["10.0.0.50".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: Some(4),
+ update_source: None,
+ }],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "MIGRATION".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ networks: vec![],
+ redistribute: vec![Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ }],
+ }),
+ ipv6_unicast: None,
+ l2vpn_evpn: None,
+ },
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_vrf_with_routes_and_global_routes() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrfs: BTreeMap::from([(
+ iface("vrf-routes"),
+ Vrf {
+ vni: Some(100),
+ ip_routes: vec![
+ IpRoute {
+ is_ipv6: false,
+ prefix: proxmox_network_types::Cidr::new_v4([10, 100, 0, 0], 24)
+ .unwrap(),
+ via: IpOrInterface::Ip("10.0.0.254".parse().unwrap()),
+ vrf: None,
+ },
+ IpRoute {
+ is_ipv6: true,
+ prefix: proxmox_network_types::Cidr::new_v6(
+ "fd10::".parse::<std::net::Ipv6Addr>().unwrap(),
+ 64,
+ )
+ .unwrap(),
+ via: IpOrInterface::Ip("fd00::1".parse().unwrap()),
+ vrf: None,
+ },
+ ],
+ custom_frr_config: vec![],
+ },
+ )]),
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: None,
+ default_ipv4_unicast: None,
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: vec![],
+ address_families: AddressFamilies::default(),
+ custom_frr_config: vec![],
+ },
+ )]),
+ ..Default::default()
+ },
+ ip_routes: vec![
+ IpRoute {
+ is_ipv6: false,
+ prefix: proxmox_network_types::Cidr::new_v4([172, 16, 0, 0], 12).unwrap(),
+ via: IpOrInterface::Ip("10.0.0.1".parse().unwrap()),
+ vrf: None,
+ },
+ IpRoute {
+ is_ipv6: true,
+ prefix: proxmox_network_types::Cidr::new_v6(
+ "2001:db8::".parse::<std::net::Ipv6Addr>().unwrap(),
+ 32,
+ )
+ .unwrap(),
+ via: IpOrInterface::Ip("fd00::1".parse().unwrap()),
+ vrf: None,
+ },
+ ],
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_bgp_maximal() {
+ let config = FrrConfig {
+ bgp: BgpFrrConfig {
+ vrf_router: BTreeMap::from([(
+ VrfName::Default,
+ BgpRouter {
+ asn: 65000,
+ router_id: "10.0.0.1".parse().unwrap(),
+ coalesce_time: Some(2000),
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: Some(false),
+ graceful_restart_notification: Some(false),
+ disable_ebgp_connected_route_check: Some(true),
+ bestpath_as_path_multipath_relax: Some(true),
+ neighbor_groups: vec![
+ NeighborGroup {
+ name: word("GROUP_A"),
+ bfd: true,
+ local_as: Some(65001),
+ remote_as: NeighborRemoteAs::External,
+ ips: vec!["10.0.0.2".parse().unwrap(), "10.0.0.3".parse().unwrap()],
+ interfaces: vec![iface("swp1")],
+ ebgp_multihop: Some(5),
+ update_source: Some(iface("lo")),
+ },
+ NeighborGroup {
+ name: word("GROUP_B"),
+ bfd: false,
+ local_as: None,
+ remote_as: NeighborRemoteAs::Internal,
+ ips: vec!["10.0.0.10".parse().unwrap()],
+ interfaces: vec![],
+ ebgp_multihop: None,
+ update_source: None,
+ },
+ ],
+ address_families: AddressFamilies {
+ ipv4_unicast: Some(Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![
+ AddressFamilyNeighbor {
+ name: "GROUP_A".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: Some(RouteMapName::new("IN4".to_string())),
+ route_map_out: Some(RouteMapName::new("OUT4".to_string())),
+ },
+ AddressFamilyNeighbor {
+ name: "GROUP_B".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ },
+ ],
+ custom_frr_config: vec![" maximum-paths 16".to_string()],
+ },
+ networks: vec![
+ Ipv4Cidr::new([10, 0, 0, 0], 8).unwrap(),
+ Ipv4Cidr::new([172, 16, 0, 0], 12).unwrap(),
+ ],
+ redistribute: vec![
+ Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ },
+ Redistribution {
+ protocol: RedistributeProtocol::Static,
+ metric: Some(50),
+ route_map: None,
+ },
+ ],
+ }),
+ ipv6_unicast: Some(Ipv6UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "GROUP_A".to_string(),
+ soft_reconfiguration_inbound: Some(true),
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![],
+ },
+ networks: vec![Ipv6Cidr::new(
+ "fd00::".parse::<std::net::Ipv6Addr>().unwrap(),
+ 48,
+ )
+ .unwrap()],
+ redistribute: vec![Redistribution {
+ protocol: RedistributeProtocol::Connected,
+ metric: None,
+ route_map: None,
+ }],
+ }),
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: vec![],
+ neighbors: vec![AddressFamilyNeighbor {
+ name: "GROUP_A".to_string(),
+ soft_reconfiguration_inbound: None,
+ route_map_in: None,
+ route_map_out: None,
+ }],
+ custom_frr_config: vec![" advertise-svi-ip".to_string()],
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: Some(true),
+ default_originate: vec![DefaultOriginate::Ipv4, DefaultOriginate::Ipv6],
+ advertise_ipv4_unicast: Some(true),
+ advertise_ipv6_unicast: Some(true),
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: vec![
+ " bgp log-neighbor-changes".to_string(),
+ " timers bgp 10 30".to_string(),
+ " bgp graceful-restart".to_string(),
+ ],
+ },
+ )]),
+ ..Default::default()
+ },
+ routemaps: BTreeMap::from([
+ (
+ RouteMapName::new("IN4".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![RouteMapSet::LocalPreference(200)],
+ custom_frr_config: vec![],
+ }],
+ ),
+ (
+ RouteMapName::new("OUT4".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![RouteMapSet::Metric(100)],
+ custom_frr_config: vec![],
+ }],
+ ),
+ ]),
+ custom_frr_config: vec![
+ "log syslog informational".to_string(),
+ "service integrated-vtysh-config".to_string(),
+ ],
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_routemap_nexthop_match_community_set() {
+ let config = FrrConfig {
+ routemaps: BTreeMap::from([(
+ RouteMapName::new("NEXTHOP_MATCH".to_string()),
+ vec![
+ RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V4(RouteMapMatchInner::NextHop(
+ "10.0.0.1".to_string(),
+ ))],
+ sets: vec![
+ RouteMapSet::Community("65000:100".to_string()),
+ RouteMapSet::LocalPreference(300),
+ ],
+ custom_frr_config: vec![" on-match next".to_string()],
+ },
+ RouteMapEntry {
+ seq: 20,
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V4(RouteMapMatchInner::NextHop(
+ "10.0.0.2".to_string(),
+ ))],
+ sets: vec![RouteMapSet::Metric(50)],
+ custom_frr_config: vec![],
+ },
+ RouteMapEntry {
+ seq: 100,
+ action: AccessAction::Deny,
+ matches: vec![],
+ sets: vec![],
+ custom_frr_config: vec![],
+ },
+ ],
+ )]),
+ custom_frr_config: vec![],
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_protocol_routemaps_multiple() {
+ let config = FrrConfig {
+ routemaps: BTreeMap::from([
+ (
+ RouteMapName::new("OSPF_POLICY".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![],
+ custom_frr_config: vec![],
+ }],
+ ),
+ (
+ RouteMapName::new("BGP_POLICY".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![],
+ custom_frr_config: vec![],
+ }],
+ ),
+ (
+ RouteMapName::new("FABRIC_POLICY".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![],
+ custom_frr_config: vec![],
+ }],
+ ),
+ ]),
+ protocol_routemaps: BTreeMap::from([
+ (
+ FrrProtocol::Ospf,
+ IpProtocolRouteMap {
+ v4: Some(RouteMapName::new("OSPF_POLICY".to_string())),
+ v6: None,
+ },
+ ),
+ (
+ FrrProtocol::Bgp,
+ IpProtocolRouteMap {
+ v4: Some(RouteMapName::new("BGP_POLICY".to_string())),
+ v6: Some(RouteMapName::new("BGP_POLICY".to_string())),
+ },
+ ),
+ (
+ FrrProtocol::Openfabric,
+ IpProtocolRouteMap {
+ v4: None,
+ v6: Some(RouteMapName::new("FABRIC_POLICY".to_string())),
+ },
+ ),
+ ]),
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
+
+#[test_with::executable(vtysh)]
+#[test]
+fn test_vtysh_routemap_set_src() {
+ let config = FrrConfig {
+ routemaps: BTreeMap::from([(
+ RouteMapName::new("SET_SRC".to_string()),
+ vec![RouteMapEntry {
+ seq: 10,
+ action: AccessAction::Permit,
+ matches: vec![],
+ sets: vec![RouteMapSet::Src("10.0.0.1".parse().unwrap())],
+ custom_frr_config: vec![],
+ }],
+ )]),
+ protocol_routemaps: BTreeMap::from([(
+ FrrProtocol::Ospf,
+ IpProtocolRouteMap {
+ v4: Some(RouteMapName::new("SET_SRC".to_string())),
+ v6: None,
+ },
+ )]),
+ ..Default::default()
+ };
+
+ dump_and_check(&config, function_name!());
+}
diff --git a/proxmox-sdn-types/src/openfabric.rs b/proxmox-sdn-types/src/openfabric.rs
index c79e2d9a2935..908ca793b2e8 100644
--- a/proxmox-sdn-types/src/openfabric.rs
+++ b/proxmox-sdn-types/src/openfabric.rs
@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use std::fmt::Display;
-use proxmox_schema::{api, UpdaterType};
+use proxmox_schema::{api, ApiType, Schema, UpdaterType};
/// The OpenFabric CSNP Interval.
///
@@ -16,6 +16,16 @@ use proxmox_schema::{api, UpdaterType};
#[serde(transparent)]
pub struct CsnpInterval(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u16")] u16);
+impl CsnpInterval {
+ pub fn new(interval: u16) -> Result<Self, anyhow::Error> {
+ match CsnpInterval::API_SCHEMA {
+ Schema::Integer(s) => s.check_constraints(interval as i64),
+ _ => unreachable!(),
+ }
+ .map(|_| Self(interval))
+ }
+}
+
impl UpdaterType for CsnpInterval {
type Updater = Option<CsnpInterval>;
}
@@ -39,6 +49,16 @@ impl Display for CsnpInterval {
#[serde(transparent)]
pub struct HelloInterval(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u16")] u16);
+impl HelloInterval {
+ pub fn new(interval: u16) -> Result<Self, anyhow::Error> {
+ match HelloInterval::API_SCHEMA {
+ Schema::Integer(s) => s.check_constraints(interval as i64),
+ _ => unreachable!(),
+ }
+ .map(|_| Self(interval))
+ }
+}
+
impl UpdaterType for HelloInterval {
type Updater = Option<HelloInterval>;
}
@@ -61,6 +81,16 @@ impl Display for HelloInterval {
#[serde(transparent)]
pub struct HelloMultiplier(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u16")] u16);
+impl HelloMultiplier {
+ pub fn new(interval: u16) -> Result<Self, anyhow::Error> {
+ match HelloMultiplier::API_SCHEMA {
+ Schema::Integer(s) => s.check_constraints(interval as i64),
+ _ => unreachable!(),
+ }
+ .map(|_| Self(interval))
+ }
+}
+
impl UpdaterType for HelloMultiplier {
type Updater = Option<HelloMultiplier>;
}
--
2.47.3
next prev parent reply other threads:[~2026-03-23 14:26 UTC|newest]
Thread overview: 40+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-23 13:48 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v7 00/21] Generate frr config using jinja templates and rust types Gabriel Goller
2026-03-23 13:48 ` [PATCH proxmox-ve-rs v7 01/21] ve-config: firewall: cargo fmt Gabriel Goller
2026-03-23 13:48 ` [PATCH proxmox-ve-rs v7 02/21] frr: add proxmox-frr-templates package that contains templates Gabriel Goller
2026-03-26 15:15 ` Shannon Sterz
2026-03-27 9:07 ` Gabriel Goller
2026-03-27 9:12 ` Gabriel Goller
2026-03-27 10:10 ` Thomas Lamprecht
2026-03-27 10:17 ` Gabriel Goller
2026-03-23 13:48 ` [PATCH proxmox-ve-rs v7 03/21] ve-config: remove FrrConfigBuilder struct Gabriel Goller
2026-03-23 13:48 ` [PATCH proxmox-ve-rs v7 04/21] sdn-types: support variable-length NET identifier Gabriel Goller
2026-03-26 15:15 ` Shannon Sterz
2026-03-27 9:25 ` Gabriel Goller
2026-03-23 13:48 ` [PATCH proxmox-ve-rs v7 05/21] frr: add template serializer and serialize fabrics using templates Gabriel Goller
2026-03-27 0:37 ` Thomas Lamprecht
2026-03-27 9:45 ` Gabriel Goller
2026-03-23 13:48 ` [PATCH proxmox-ve-rs v7 06/21] frr: add isis configuration and templates Gabriel Goller
2026-03-27 0:41 ` Thomas Lamprecht
2026-03-27 9:53 ` Gabriel Goller
2026-03-23 13:48 ` [PATCH proxmox-ve-rs v7 07/21] frr: support custom frr configuration lines Gabriel Goller
2026-03-23 13:48 ` [PATCH proxmox-ve-rs v7 08/21] frr: add bgp support with templates and serialization Gabriel Goller
2026-03-27 0:50 ` Thomas Lamprecht
2026-03-27 10:05 ` Gabriel Goller
2026-03-23 13:48 ` [PATCH proxmox-ve-rs v7 09/21] frr: enable minijinja strict undefined behavior mode Gabriel Goller
2026-03-23 13:49 ` Gabriel Goller [this message]
2026-03-24 9:03 ` [PATCH proxmox-ve-rs v7 10/21] frr: add vtysh integration tests for proxmox-frr Gabriel Goller
2026-03-23 13:49 ` [PATCH proxmox-perl-rs v7 11/21] sdn: add function to generate the frr config for all daemons Gabriel Goller
2026-03-23 13:49 ` [PATCH pve-network v7 12/21] tests: use Test::Differences to make test assertions Gabriel Goller
2026-03-23 13:49 ` [PATCH pve-network v7 13/21] test: add tests for frr.conf.local merging Gabriel Goller
2026-03-23 13:49 ` [PATCH pve-network v7 14/21] test: bgp: add some various integration tests Gabriel Goller
2026-03-23 13:49 ` [PATCH pve-network v7 15/21] sdn: write structured frr config that can be rendered using templates Gabriel Goller
2026-03-23 13:49 ` [PATCH pve-network v7 16/21] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
2026-03-23 13:49 ` [PATCH pve-network v7 17/21] tests: rearrange some statements in the " Gabriel Goller
2026-03-23 13:49 ` [PATCH pve-network v7 18/21] sdn: adjust frr.conf.local merging to rust template types Gabriel Goller
2026-03-23 13:49 ` [PATCH pve-network v7 19/21] test: adjust frr_local_merge test for new template generation Gabriel Goller
2026-03-23 13:49 ` [PATCH pve-network v7 20/21] api: add dry-run endpoint for sdn apply to preview changes Gabriel Goller
2026-03-27 1:07 ` Thomas Lamprecht
2026-03-23 13:49 ` [PATCH pve-manager v7 21/21] sdn: add dry-run diff view for sdn apply Gabriel Goller
2026-03-26 13:40 ` [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v7 00/21] Generate frr config using jinja templates and rust types Wolfgang Bumiller
2026-03-27 1:11 ` Thomas Lamprecht
2026-03-27 10:06 ` 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=20260323134934.243110-11-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox