From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 1053B1FF136 for ; Mon, 23 Mar 2026 15:26:42 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id CAA391E096; Mon, 23 Mar 2026 15:27:00 +0100 (CET) From: Gabriel Goller 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 Message-ID: <20260323134934.243110-11-g.goller@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260323134934.243110-1-g.goller@proxmox.com> References: <20260323134934.243110-1-g.goller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1774273736114 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.023 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record X-MailFrom: g.goller@proxmox.com X-Mailman-Rule-Hits: max-size X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; news-moderation; no-subject; digests; suspicious-header Message-ID-Hash: ZCBQMIIRB5K7KPWEAY7L2KP2CC7F6JLK X-Message-ID-Hash: ZCBQMIIRB5K7KPWEAY7L2KP2CC7F6JLK X-Mailman-Approved-At: Mon, 23 Mar 2026 15:26:49 +0100 X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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` 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 Tested-by: Stefan Hanreich Signed-off-by: Gabriel Goller --- 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, - ipv6_unicast: Option, - l2vpn_evpn: Option, + pub ipv4_unicast: Option, + pub ipv6_unicast: Option, + pub l2vpn_evpn: Option, } #[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 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, + pub is_ipv6: bool, + pub prefix: Cidr, + pub via: IpOrInterface, + pub vrf: Option, } #[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::().unwrap(), 64) + .unwrap(), + Ipv6Cidr::new( + "2001:db8::".parse::().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) -> &'static str { + std::any::type_name::() + } + 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::().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::().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::().unwrap(), 64) + .unwrap(), + Ipv6Cidr::new("2001:db8::1".parse::().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::().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::().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::().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::().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::().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, + route_map_out: Option, +) -> 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::().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::().unwrap(), 64) + .unwrap(), + Ipv6Cidr::new( + "2001:db8::".parse::().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::().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::().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::().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::().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::().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::().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::().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 { + match CsnpInterval::API_SCHEMA { + Schema::Integer(s) => s.check_constraints(interval as i64), + _ => unreachable!(), + } + .map(|_| Self(interval)) + } +} + impl UpdaterType for CsnpInterval { type Updater = Option; } @@ -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 { + match HelloInterval::API_SCHEMA { + Schema::Integer(s) => s.check_constraints(interval as i64), + _ => unreachable!(), + } + .map(|_| Self(interval)) + } +} + impl UpdaterType for HelloInterval { type Updater = Option; } @@ -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 { + match HelloMultiplier::API_SCHEMA { + Schema::Integer(s) => s.check_constraints(interval as i64), + _ => unreachable!(), + } + .map(|_| Self(interval)) + } +} + impl UpdaterType for HelloMultiplier { type Updater = Option; } -- 2.47.3