From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 5F7471FF0DF for ; Fri, 28 Aug 2026 13:35:14 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 585B32163F; Fri, 28 Aug 2026 13:34:21 +0200 (CEST) From: Gabriel Goller To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-ve-rs v3 02/13] ve-config: add IS-IS fabric config parsing and frr config generation Date: Fri, 28 Aug 2026 13:32:41 +0200 Message-ID: <20260828113255.176546-3-g.goller@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260828113255.176546-1-g.goller@proxmox.com> References: <20260828113255.176546-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: 1787916770394 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.399 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: FC2CBNEJXVCJ6RYL6C464JPHBDFFXPVE X-Message-ID-Hash: FC2CBNEJXVCJ6RYL6C464JPHBDFFXPVE X-MailFrom: g.goller@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header 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 the necessary types to parse IS-IS fabrics from the fabrics.cfg config file and convert that config into the frr config types. Everything is quite similar to OpenFabric, but it's worth to keep them separate because they will diverge in the future. Signed-off-by: Gabriel Goller --- proxmox-ve-config/src/sdn/fabric/frr.rs | 224 +++++++++++++++++- proxmox-ve-config/src/sdn/fabric/mod.rs | 109 +++++++++ .../src/sdn/fabric/section_config/fabric.rs | 22 ++ .../src/sdn/fabric/section_config/mod.rs | 19 ++ .../src/sdn/fabric/section_config/node.rs | 21 ++ .../fabric/section_config/protocol/isis.rs | 159 +++++++++++++ .../sdn/fabric/section_config/protocol/mod.rs | 1 + 7 files changed, 554 insertions(+), 1 deletion(-) create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/isis.rs diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs index 9203dcd16dae..5dee4796173e 100644 --- a/proxmox-ve-config/src/sdn/fabric/frr.rs +++ b/proxmox-ve-config/src/sdn/fabric/frr.rs @@ -1,5 +1,6 @@ use std::net::{IpAddr, Ipv4Addr}; +use proxmox_frr::ser::isis::{IsisInterface, IsisRouter, IsisRouterName}; use tracing; use proxmox_frr::ser::bgp::{ @@ -21,6 +22,7 @@ use crate::common::valid::Valid; use crate::sdn::fabric::section_config::protocol::bgp::{BgpNode, bgp_router_id}; use crate::sdn::fabric::section_config::protocol::{ bgp::BgpRedistributionSource, + isis::{IsisInterfaceProperties, IsisProperties}, openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties}, ospf::OspfInterfaceProperties, }; @@ -198,6 +200,160 @@ pub fn build_fabric( protocol_routemap.v6 = Some(routemap_name) } } + FabricEntry::Isis(isis_entry) => { + // Get the current node of this fabric, if it doesn't exist, skip this fabric and + // don't generate any FRR config. + let Ok(node) = isis_entry.node_section(¤t_node) else { + continue; + }; + + if current_net.is_none() { + current_net = match (node.ip(), node.ip6()) { + (Some(ip), _) => Some(ip.into()), + (_, Some(ip6)) => Some(ip6.into()), + (_, _) => None, + } + } + + let net = current_net + .as_ref() + .ok_or_else(|| anyhow::anyhow!("no IPv4 or IPv6 set for node"))?; + let (router_name, router_item) = build_isis_router(fabric_id, net.clone())?; + + if frr_config + .isis + .router + .insert(router_name, router_item) + .is_some() + { + tracing::error!("duplicate IS-IS router"); + } + + // Create dummy interface for fabric + let (interface, interface_name) = build_isis_dummy_interface( + fabric_id, + node.ip().is_some(), + node.ip6().is_some(), + )?; + + if frr_config + .isis + .interfaces + .insert(interface_name, interface) + .is_some() + { + tracing::error!( + "An interface with the same name as the dummy interface exists" + ); + } + + let fabric = isis_entry.fabric_section(); + + for interface in node.properties().interfaces.iter() { + let (interface, interface_name) = build_isis_interface( + fabric_id, + interface, + fabric.properties(), + node.ip().is_some(), + node.ip6().is_some(), + )?; + + if frr_config + .isis + .interfaces + .insert(interface_name, interface) + .is_some() + { + tracing::warn!("An interface cannot be in multiple IS-IS fabrics"); + } + } + + if let Some(ip) = node.ip() { + let routemap_name = ser::route_map::RouteMapName::new("pve_isis".to_owned()); + let routemap = frr_config + .routemaps + .entry(routemap_name.clone()) + .or_default(); + + let mut routemap_entry = build_source_routemap(ip.into(), routemap_seq); + routemap_seq += 10; + + if let Some(prefix_list_id) = &fabric.properties().route_filter { + routemap_entry.matches = vec![RouteMapMatch::IpAddressPrefixList( + prefix_list_id.clone().into(), + )]; + } else if let Some(cidr) = fabric.ip_prefix() { + let access_list_name = + AccessListName::new(format!("pve_isis_{fabric_id}_ips")); + + let rule = ser::route_map::AccessListRule { + action: ser::route_map::AccessAction::Permit, + network: Cidr::from(cidr), + is_ipv6: false, + seq: None, + }; + + frr_config + .access_lists + .insert(access_list_name.clone(), vec![rule]); + + routemap_entry.matches = + vec![RouteMapMatch::IpAddressAccessList(access_list_name)]; + } + + routemap.push(routemap_entry); + + let protocol_routemap = frr_config + .protocol_routemaps + .entry(FrrProtocol::Isis) + .or_default(); + + protocol_routemap.v4 = Some(routemap_name) + } + + if let Some(ip) = node.ip6() { + let routemap_name = ser::route_map::RouteMapName::new("pve_isis6".to_owned()); + let routemap = frr_config + .routemaps + .entry(routemap_name.clone()) + .or_default(); + + let mut routemap_entry = build_source_routemap(ip.into(), routemap_seq); + routemap_seq += 10; + + if let Some(prefix_list_id) = &fabric.properties().route_filter { + routemap_entry.matches = vec![RouteMapMatch::Ip6AddressPrefixList( + prefix_list_id.clone().into(), + )]; + } else if let Some(cidr) = fabric.ip6_prefix() { + let access_list_name = + AccessListName::new(format!("pve_isis_{fabric_id}_ip6s")); + + let rule = ser::route_map::AccessListRule { + action: ser::route_map::AccessAction::Permit, + network: Cidr::from(cidr), + is_ipv6: true, + seq: None, + }; + + frr_config + .access_lists + .insert(access_list_name.clone(), vec![rule]); + + routemap_entry.matches = + vec![RouteMapMatch::Ip6AddressAccessList(access_list_name)]; + } + + routemap.push(routemap_entry); + + let protocol_routemap = frr_config + .protocol_routemaps + .entry(FrrProtocol::Isis) + .or_default(); + + protocol_routemap.v6 = Some(routemap_name) + } + } FabricEntry::Ospf(ospf_entry) => { let Ok(node) = ospf_entry.node_section(¤t_node) else { continue; @@ -611,6 +767,22 @@ fn build_openfabric_router( Ok((router_name, router_item)) } +/// Helper that builds a IS-IS router from a fabric_id and a [`Net`]. +fn build_isis_router( + fabric_id: &FabricId, + net: Net, +) -> Result<(IsisRouterName, IsisRouter), anyhow::Error> { + let frr_router = IsisRouter { + net, + log_adjacency_changes: None, + redistribute: None, + custom_frr_config: Vec::new(), + }; + let frr_word_id = ser::FrrWord::new(fabric_id.to_string())?; + let router_name = IsisRouterName::new(frr_word_id); + Ok((router_name, frr_router)) +} + /// Helper that builds a OSPF interface from an [`ospf::Area`] and the [`OspfInterfaceProperties`]. fn build_ospf_interface( area: ser::ospf::Area, @@ -702,7 +874,7 @@ fn build_openfabric_dummy_interface( Ok((frr_interface.into(), interface_name)) } -/// Helper that builds a RouteMap for the OpenFabric protocol. +/// Helper that builds a RouteMap for the OpenFabric/ISIS protocol. fn build_source_routemap(router_ip: IpAddr, seq: u16) -> RouteMapEntry { RouteMapEntry { seq, @@ -714,3 +886,53 @@ fn build_source_routemap(router_ip: IpAddr, seq: u16) -> RouteMapEntry { exit_action: None, } } + +/// Helper that builds the IS-IS interface. +/// +/// Takes the [`FabricId`], [`IsisInterfaceProperties`], [`IsisProperties`] and flags for +/// ipv4 and ipv6. +fn build_isis_interface( + fabric_id: &FabricId, + interface: &IsisInterfaceProperties, + fabric_config: &IsisProperties, + is_ipv4: bool, + is_ipv6: bool, +) -> Result<(Interface, InterfaceName), anyhow::Error> { + let frr_word = ser::FrrWord::new(fabric_id.to_string())?; + let frr_interface = IsisInterface { + fabric_id: frr_word.into(), + hello_interval: fabric_config.hello_interval, + csnp_interval: fabric_config.csnp_interval, + hello_multiplier: interface.hello_multiplier, + passive: None, + is_ipv4, + is_ipv6, + point_to_point: interface.ip.is_none() && interface.ip6.is_none(), + custom_frr_config: Vec::new(), + }; + + let interface_name = interface.name.as_str().try_into()?; + Ok((frr_interface.into(), interface_name)) +} + +/// Helper that builds a IS-IS interface using a [`FabricId`] and ipv4/6 flags. +fn build_isis_dummy_interface( + fabric_id: &FabricId, + is_ipv4: bool, + is_ipv6: bool, +) -> Result<(Interface, InterfaceName), anyhow::Error> { + let frr_word = ser::FrrWord::new(fabric_id.to_string())?; + let frr_interface = IsisInterface { + fabric_id: frr_word.into(), + hello_interval: None, + csnp_interval: None, + hello_multiplier: None, + passive: Some(true), + is_ipv4, + is_ipv6, + point_to_point: false, + custom_frr_config: Vec::new(), + }; + let interface_name = format!("dummy_{}", fabric_id).try_into()?; + Ok((frr_interface.into(), interface_name)) +} diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs index 22f19c7af2e1..4493f10ca4b3 100644 --- a/proxmox-ve-config/src/sdn/fabric/mod.rs +++ b/proxmox-ve-config/src/sdn/fabric/mod.rs @@ -25,6 +25,10 @@ use crate::sdn::fabric::section_config::protocol::bgp::{ BgpDeletableProperties, BgpNode, BgpNodeDeletableProperties, BgpNodePropertiesUpdater, BgpProperties, BgpPropertiesUpdater, bgp_router_id, }; +use crate::sdn::fabric::section_config::protocol::isis::{ + IsisDeletableProperties, IsisNodeDeletableProperties, IsisNodeProperties, + IsisNodePropertiesUpdater, IsisProperties, IsisPropertiesUpdater, +}; use crate::sdn::fabric::section_config::protocol::openfabric::{ OpenfabricDeletableProperties, OpenfabricNodeDeletableProperties, OpenfabricNodeProperties, OpenfabricNodePropertiesUpdater, OpenfabricProperties, OpenfabricPropertiesUpdater, @@ -211,6 +215,7 @@ macro_rules! impl_entry { } impl_entry!(Openfabric, OpenfabricProperties, OpenfabricNodeProperties); +impl_entry!(Isis, IsisProperties, IsisNodeProperties); impl_entry!(Ospf, OspfProperties, OspfNodeProperties); impl_entry!(WireGuard, WireGuardProperties, WireGuardNode); impl_entry!(Bgp, BgpProperties, BgpNode); @@ -222,6 +227,7 @@ impl_entry!(Bgp, BgpProperties, BgpNode); #[derive(Debug, Clone, Serialize, Deserialize, Hash)] pub enum FabricEntry { Openfabric(Entry), + Isis(Entry), Ospf(Entry), WireGuard(Entry), Bgp(Entry), @@ -235,6 +241,7 @@ impl FabricEntry { (FabricEntry::Openfabric(entry), Node::Openfabric(node_section)) => { entry.add_node(node_section) } + (FabricEntry::Isis(entry), Node::Isis(node_section)) => entry.add_node(node_section), (FabricEntry::Ospf(entry), Node::Ospf(node_section)) => entry.add_node(node_section), (FabricEntry::WireGuard(entry), Node::WireGuard(node_section)) => { entry.add_node(node_section) @@ -249,6 +256,7 @@ impl FabricEntry { pub fn get_node(&self, id: &NodeId) -> Result<&Node, FabricConfigError> { match self { FabricEntry::Openfabric(entry) => entry.get_node(id), + FabricEntry::Isis(entry) => entry.get_node(id), FabricEntry::Ospf(entry) => entry.get_node(id), FabricEntry::WireGuard(entry) => entry.get_node(id), FabricEntry::Bgp(entry) => entry.get_node(id), @@ -260,6 +268,7 @@ impl FabricEntry { pub fn get_node_mut(&mut self, id: &NodeId) -> Result<&mut Node, FabricConfigError> { match self { FabricEntry::Openfabric(entry) => entry.get_node_mut(id), + FabricEntry::Isis(entry) => entry.get_node_mut(id), FabricEntry::Ospf(entry) => entry.get_node_mut(id), FabricEntry::WireGuard(entry) => entry.get_node_mut(id), FabricEntry::Bgp(entry) => entry.get_node_mut(id), @@ -310,6 +319,38 @@ impl FabricEntry { Ok(()) } + (Node::Isis(node_section), NodeUpdater::Isis(updater)) => { + let NodeDataUpdater:: { + ip, + ip6, + properties: IsisNodePropertiesUpdater { interfaces }, + delete, + } = updater; + + if let Some(ip) = ip { + node_section.ip = Some(ip); + } + + if let Some(ip) = ip6 { + node_section.ip6 = Some(ip); + } + + if let Some(interfaces) = interfaces { + node_section.properties.interfaces = interfaces; + } + + for property in delete { + match property { + NodeDeletableProperties::Ip => node_section.ip = None, + NodeDeletableProperties::Ip6 => node_section.ip6 = None, + NodeDeletableProperties::Protocol( + IsisNodeDeletableProperties::Interfaces, + ) => node_section.properties.interfaces = Vec::new(), + } + } + + Ok(()) + } (Node::Ospf(node_section), NodeUpdater::Ospf(updater)) => { let NodeDataUpdater:: { ip, @@ -495,6 +536,7 @@ impl FabricEntry { pub fn nodes(&self) -> impl Iterator + '_ { match self { FabricEntry::Openfabric(entry) => entry.nodes.iter(), + FabricEntry::Isis(entry) => entry.nodes.iter(), FabricEntry::Ospf(entry) => entry.nodes.iter(), FabricEntry::WireGuard(entry) => entry.nodes.iter(), FabricEntry::Bgp(entry) => entry.nodes.iter(), @@ -505,6 +547,7 @@ impl FabricEntry { pub fn delete_node(&mut self, id: &NodeId) -> Result { match self { FabricEntry::Openfabric(entry) => entry.delete_node(id), + FabricEntry::Isis(entry) => entry.delete_node(id), FabricEntry::Ospf(entry) => entry.delete_node(id), FabricEntry::WireGuard(entry) => entry.delete_node(id), FabricEntry::Bgp(entry) => entry.delete_node(id), @@ -516,6 +559,7 @@ impl FabricEntry { pub fn into_section_config(self) -> (Fabric, Vec) { match self { FabricEntry::Openfabric(entry) => entry.into_pair(), + FabricEntry::Isis(entry) => entry.into_pair(), FabricEntry::Ospf(entry) => entry.into_pair(), FabricEntry::WireGuard(entry) => entry.into_pair(), FabricEntry::Bgp(entry) => entry.into_pair(), @@ -526,6 +570,7 @@ impl FabricEntry { pub fn fabric(&self) -> &Fabric { match self { FabricEntry::Openfabric(entry) => &entry.fabric, + FabricEntry::Isis(entry) => &entry.fabric, FabricEntry::Ospf(entry) => &entry.fabric, FabricEntry::WireGuard(entry) => &entry.fabric, FabricEntry::Bgp(entry) => &entry.fabric, @@ -536,6 +581,7 @@ impl FabricEntry { pub fn fabric_mut(&mut self) -> &mut Fabric { match self { FabricEntry::Openfabric(entry) => &mut entry.fabric, + FabricEntry::Isis(entry) => &mut entry.fabric, FabricEntry::Ospf(entry) => &mut entry.fabric, FabricEntry::WireGuard(entry) => &mut entry.fabric, FabricEntry::Bgp(entry) => &mut entry.fabric, @@ -549,6 +595,7 @@ impl From for FabricEntry { Fabric::Openfabric(fabric_section) => { FabricEntry::Openfabric(Entry::new(fabric_section)) } + Fabric::Isis(fabric_section) => FabricEntry::Isis(Entry::new(fabric_section)), Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)), Fabric::WireGuard(fabric_section) => FabricEntry::WireGuard(Entry::new(fabric_section)), Fabric::Bgp(fabric_section) => FabricEntry::Bgp(Entry::new(fabric_section)), @@ -867,6 +914,13 @@ impl Validatable for FabricConfig { } } } + Node::Isis(node_section) => { + if !node_section.properties().interfaces().all(|interface| { + node_interfaces.insert((node_id, interface.name.as_str())) + }) { + return Err(FabricConfigError::DuplicateInterface); + } + } } } @@ -996,6 +1050,61 @@ impl FabricConfig { Ok(()) } + (Fabric::Isis(fabric_section), FabricUpdater::Isis(updater)) => { + let FabricSectionUpdater:: { + ip_prefix, + ip6_prefix, + properties: + IsisPropertiesUpdater { + hello_interval, + csnp_interval, + route_filter, + }, + delete, + } = updater; + + if let Some(prefix) = ip_prefix { + fabric_section.ip_prefix = Some(prefix); + } + + if let Some(prefix) = ip6_prefix { + fabric_section.ip6_prefix = Some(prefix); + } + + if let Some(hello_interval) = hello_interval { + fabric_section.properties.hello_interval = Some(hello_interval); + } + + if let Some(csnp_interval) = csnp_interval { + fabric_section.properties.csnp_interval = Some(csnp_interval); + } + + if let Some(route_filter) = route_filter { + fabric_section.properties.route_filter = Some(route_filter); + } + + for property in delete { + match property { + FabricDeletableProperties::IpPrefix => { + fabric_section.ip_prefix = None; + } + FabricDeletableProperties::Ip6Prefix => { + fabric_section.ip6_prefix = None; + } + FabricDeletableProperties::Protocol( + IsisDeletableProperties::CsnpInterval, + ) => fabric_section.properties.csnp_interval = None, + FabricDeletableProperties::Protocol( + IsisDeletableProperties::HelloInterval, + ) => fabric_section.properties.hello_interval = None, + FabricDeletableProperties::Protocol( + IsisDeletableProperties::RouteFilter, + ) => fabric_section.properties.route_filter = None, + } + } + + Ok(()) + } (Fabric::Ospf(fabric_section), FabricUpdater::Ospf(updater)) => { let FabricSectionUpdater:: { ip_prefix, diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs index 9d380c524d72..86479dd299f7 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs @@ -12,6 +12,9 @@ use crate::sdn::fabric::FabricConfigError; use crate::sdn::fabric::section_config::protocol::bgp::{ BgpDeletableProperties, BgpProperties, BgpPropertiesUpdater, }; +use crate::sdn::fabric::section_config::protocol::isis::{ + IsisDeletableProperties, IsisProperties, IsisPropertiesUpdater, +}; use crate::sdn::fabric::section_config::protocol::openfabric::{ OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater, }; @@ -142,6 +145,10 @@ impl UpdaterType for FabricSection { type Updater = FabricSectionUpdater; } +impl UpdaterType for FabricSection { + type Updater = FabricSectionUpdater; +} + impl UpdaterType for FabricSection { type Updater = FabricSectionUpdater; } @@ -173,6 +180,7 @@ impl UpdaterType for FabricSection { #[serde(rename_all = "snake_case", tag = "protocol")] pub enum Fabric { Openfabric(FabricSection), + Isis(FabricSection), Ospf(FabricSection), #[serde(rename = "wireguard")] WireGuard(FabricSection), @@ -190,6 +198,7 @@ impl Fabric { pub fn id(&self) -> &FabricId { match self { Self::Openfabric(fabric_section) => fabric_section.id(), + Self::Isis(fabric_section) => fabric_section.id(), Self::Ospf(fabric_section) => fabric_section.id(), Self::WireGuard(fabric_section) => fabric_section.id(), Self::Bgp(fabric_section) => fabric_section.id(), @@ -202,6 +211,7 @@ impl Fabric { pub fn ip_prefix(&self) -> Option { match self { Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(), + Fabric::Isis(fabric_section) => fabric_section.ip_prefix(), Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(), Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix(), Fabric::Bgp(fabric_section) => fabric_section.ip_prefix(), @@ -214,6 +224,7 @@ impl Fabric { pub fn set_ip_prefix(&mut self, ipv4_cidr: Ipv4Cidr) { match self { Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), + Fabric::Isis(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), Fabric::Ospf(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), Fabric::Bgp(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), @@ -226,6 +237,7 @@ impl Fabric { pub fn ip6_prefix(&self) -> Option { match self { Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(), + Fabric::Isis(fabric_section) => fabric_section.ip6_prefix(), Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(), Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix(), Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix(), @@ -238,6 +250,7 @@ impl Fabric { pub fn set_ip6_prefix(&mut self, ipv6_cidr: Ipv6Cidr) { match self { Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), + Fabric::Isis(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), @@ -252,6 +265,7 @@ impl Validatable for Fabric { fn validate(&self) -> Result<(), Self::Error> { match self { Fabric::Openfabric(fabric_section) => fabric_section.validate(), + Fabric::Isis(fabric_section) => fabric_section.validate(), Fabric::Ospf(fabric_section) => fabric_section.validate(), Fabric::WireGuard(_fabric_section) => Ok(()), Fabric::Bgp(fabric_section) => fabric_section.validate(), @@ -265,6 +279,12 @@ impl From> for Fabric { } } +impl From> for Fabric { + fn from(section: FabricSection) -> Self { + Fabric::Isis(section) + } +} + impl From> for Fabric { fn from(section: FabricSection) -> Self { Fabric::Ospf(section) @@ -288,6 +308,7 @@ impl From> for Fabric { #[serde(rename_all = "snake_case", tag = "protocol")] pub enum FabricUpdater { Openfabric( as UpdaterType>::Updater), + Isis( as UpdaterType>::Updater), Ospf( as UpdaterType>::Updater), #[serde(rename = "wireguard")] WireGuard( as UpdaterType>::Updater), @@ -298,6 +319,7 @@ impl Updater for FabricUpdater { fn is_empty(&self) -> bool { match self { FabricUpdater::Openfabric(updater) => updater.is_empty(), + FabricUpdater::Isis(updater) => updater.is_empty(), FabricUpdater::Ospf(updater) => updater.is_empty(), FabricUpdater::WireGuard(updater) => updater.is_empty(), FabricUpdater::Bgp(updater) => updater.is_empty(), diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs index 35057c7a9bc5..0b832e3e3a5d 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs @@ -12,6 +12,7 @@ use crate::sdn::fabric::section_config::{ node::{NODE_ID_REGEX_STR, Node, NodeSection}, protocol::{ bgp::{BgpNode, BgpProperties}, + isis::{IsisNodeProperties, IsisProperties}, openfabric::{OpenfabricNodeProperties, OpenfabricProperties}, ospf::{OspfNodeProperties, OspfProperties}, wireguard::WireGuardNode, @@ -33,10 +34,12 @@ impl From
for FabricOrNode { fn from(section: Section) -> Self { match section { Section::OpenfabricFabric(fabric_section) => Self::Fabric(fabric_section.into()), + Section::IsisFabric(fabric_section) => Self::Fabric(fabric_section.into()), Section::OspfFabric(fabric_section) => Self::Fabric(fabric_section.into()), Section::WireGuardFabric(fabric_section) => Self::Fabric(fabric_section.into()), Section::BgpFabric(fabric_section) => Self::Fabric(fabric_section.into()), Section::OpenfabricNode(node_section) => Self::Node(node_section.into()), + Section::IsisNode(node_section) => Self::Node(node_section.into()), Section::OspfNode(node_section) => Self::Node(node_section.into()), Section::WireGuardNode(node_section) => Self::Node(node_section.into()), Section::BgpNode(node_section) => Self::Node(node_section.into()), @@ -68,11 +71,13 @@ pub const SECTION_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&SECTION #[serde(rename_all = "snake_case", tag = "type")] pub enum Section { OpenfabricFabric(FabricSection), + IsisFabric(FabricSection), OspfFabric(FabricSection), #[serde(rename = "wireguard_fabric")] WireGuardFabric(FabricSection), BgpFabric(FabricSection), OpenfabricNode(NodeSection), + IsisNode(NodeSection), OspfNode(NodeSection), #[serde(rename = "wireguard_node")] WireGuardNode(NodeSection), @@ -85,6 +90,12 @@ impl From> for Section { } } +impl From> for Section { + fn from(section: FabricSection) -> Self { + Self::IsisFabric(section) + } +} + impl From> for Section { fn from(section: FabricSection) -> Self { Self::OspfFabric(section) @@ -109,6 +120,12 @@ impl From> for Section { } } +impl From> for Section { + fn from(section: NodeSection) -> Self { + Self::IsisNode(section) + } +} + impl From> for Section { fn from(section: NodeSection) -> Self { Self::OspfNode(section) @@ -131,6 +148,7 @@ impl From for Section { fn from(fabric: Fabric) -> Self { match fabric { Fabric::Openfabric(fabric_section) => fabric_section.into(), + Fabric::Isis(fabric_section) => fabric_section.into(), Fabric::Ospf(fabric_section) => fabric_section.into(), Fabric::WireGuard(fabric_section) => fabric_section.into(), Fabric::Bgp(fabric_section) => fabric_section.into(), @@ -142,6 +160,7 @@ impl From for Section { fn from(node: Node) -> Self { match node { Node::Openfabric(node_section) => node_section.into(), + Node::Isis(node_section) => node_section.into(), Node::Ospf(node_section) => node_section.into(), Node::WireGuard(node_section) => node_section.into(), Node::Bgp(node_section) => node_section.into(), diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs index d22a547b2fa2..b9c1e35b3ea7 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs @@ -12,6 +12,7 @@ use proxmox_schema::{ use crate::common::valid::Validatable; use crate::sdn::fabric::FabricConfigError; use crate::sdn::fabric::section_config::protocol::bgp::BgpNode; +use crate::sdn::fabric::section_config::protocol::isis::IsisNodeProperties; use crate::sdn::fabric::section_config::protocol::wireguard::WireGuardNode; use crate::sdn::fabric::section_config::{ fabric::{FABRIC_ID_REGEX_STR, FabricId}, @@ -189,6 +190,7 @@ impl ApiType for NodeSection { #[serde(rename_all = "snake_case", tag = "protocol")] pub enum Node { Openfabric(NodeSection), + Isis(NodeSection), Ospf(NodeSection), #[serde(rename = "wireguard")] WireGuard(NodeSection), @@ -200,6 +202,7 @@ impl Node { pub fn id(&self) -> &NodeSectionId { match self { Node::Openfabric(node_section) => node_section.id(), + Node::Isis(node_section) => node_section.id(), Node::Ospf(node_section) => node_section.id(), Node::WireGuard(node_section) => node_section.id(), Node::Bgp(node_section) => node_section.id(), @@ -210,6 +213,7 @@ impl Node { pub fn ip(&self) -> Option { match self { Node::Openfabric(node_section) => node_section.ip(), + Node::Isis(node_section) => node_section.ip(), Node::Ospf(node_section) => node_section.ip(), Node::WireGuard(node_section) => node_section.ip(), Node::Bgp(node_section) => node_section.ip(), @@ -220,6 +224,7 @@ impl Node { pub fn ip6(&self) -> Option { match self { Node::Openfabric(node_section) => node_section.ip6(), + Node::Isis(node_section) => node_section.ip6(), Node::Ospf(node_section) => node_section.ip6(), Node::WireGuard(node_section) => node_section.ip6(), Node::Bgp(node_section) => node_section.ip6(), @@ -233,6 +238,7 @@ impl Validatable for Node { fn validate(&self) -> Result<(), Self::Error> { match self { Node::Openfabric(node_section) => node_section.validate(), + Node::Isis(node_section) => node_section.validate(), Node::Ospf(node_section) => node_section.validate(), Node::WireGuard(node_section) => node_section.validate(), Node::Bgp(node_section) => node_section.validate(), @@ -246,6 +252,12 @@ impl From> for Node { } } +impl From> for Node { + fn from(value: NodeSection) -> Self { + Self::Isis(value) + } +} + impl From> for Node { fn from(value: NodeSection) -> Self { Self::Ospf(value) @@ -286,6 +298,7 @@ pub mod api { use crate::sdn::fabric::section_config::protocol::{ bgp::{BgpNodeDeletableProperties, BgpNodePropertiesUpdater}, + isis::{IsisNodeDeletableProperties, IsisNodePropertiesUpdater}, openfabric::{ OpenfabricNodeDeletableProperties, OpenfabricNodeProperties, OpenfabricNodePropertiesUpdater, @@ -348,6 +361,7 @@ pub mod api { #[serde(rename_all = "snake_case", tag = "protocol")] pub enum Node { Openfabric(NodeData), + Isis(NodeData), Ospf(NodeData), #[serde(rename = "wireguard")] WireGuard(NodeData), @@ -358,6 +372,7 @@ pub mod api { fn from(value: super::Node) -> Self { match value { super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()), + super::Node::Isis(node_section) => Self::Isis(node_section.into()), super::Node::Ospf(node_section) => Self::Ospf(node_section.into()), super::Node::WireGuard(node_section) => Self::WireGuard(node_section.into()), super::Node::Bgp(node_section) => Self::Bgp(node_section.into()), @@ -369,6 +384,7 @@ pub mod api { fn from(value: Node) -> Self { match value { Node::Openfabric(node_section) => Self::Openfabric(node_section.into()), + Node::Isis(node_section) => Self::Isis(node_section.into()), Node::Ospf(node_section) => Self::Ospf(node_section.into()), Node::WireGuard(node_section) => Self::WireGuard(node_section.into()), Node::Bgp(node_section) => Self::Bgp(node_section.into()), @@ -381,6 +397,10 @@ pub mod api { NodeDataUpdater; } + impl UpdaterType for NodeData { + type Updater = NodeDataUpdater; + } + impl UpdaterType for NodeData { type Updater = NodeDataUpdater; } @@ -427,6 +447,7 @@ pub mod api { Openfabric( NodeDataUpdater, ), + Isis(NodeDataUpdater), Ospf(NodeDataUpdater), #[serde(rename = "wireguard")] WireGuard(NodeDataUpdater), diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/isis.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/isis.rs new file mode 100644 index 000000000000..83a5e520c812 --- /dev/null +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/isis.rs @@ -0,0 +1,159 @@ +use std::ops::{Deref, DerefMut}; + +use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr}; +use serde::{Deserialize, Serialize}; + +use proxmox_schema::{ApiStringFormat, Updater, api, property_string::PropertyString}; +use proxmox_sdn_types::openfabric::{CsnpInterval, HelloInterval, HelloMultiplier}; + +use crate::common::valid::Validatable; +use crate::sdn::fabric::FabricConfigError; +use crate::sdn::fabric::section_config::fabric::FabricSection; +use crate::sdn::fabric::section_config::interface::InterfaceName; +use crate::sdn::fabric::section_config::node::NodeSection; +use crate::sdn::prefix_list::PrefixListId; + +/// Protocol-specific options for an IS-IS Fabric. +#[api] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +pub struct IsisProperties { + /// This will be distributed to all interfaces on every node. The Hello Interval for a given + /// interface in seconds. The range is 1 to 600. Hello packets are used to establish and + /// maintain adjacency between IS-IS neighbors. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) hello_interval: Option, + + /// This will be distributed to all interfaces on every node.The Complete Sequence Number + /// Packets (CSNP) interval in seconds. The interval range is 1 to 600. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) csnp_interval: Option, + + /// By default only routes from the configured IP prefix are imported into the local routing + /// table. This setting can be used to override the allowed IPs and import additional routes + /// besides the configured IP prefix. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) route_filter: Option, +} + +impl Validatable for FabricSection { + type Error = FabricConfigError; + + /// Validates the [`FabricSection`]. + /// + /// Checks if we have either IPv4-prefix or IPv6-prefix. If both are not set, return an error. + fn validate(&self) -> Result<(), Self::Error> { + if self.ip_prefix().is_none() && self.ip6_prefix().is_none() { + return Err(FabricConfigError::FabricNoIpPrefix(self.id().to_string())); + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Hash)] +#[serde(rename_all = "snake_case")] +pub enum IsisDeletableProperties { + HelloInterval, + CsnpInterval, + RouteFilter, +} + +/// Properties for an IS-IS node +#[api( + properties: { + interfaces: { + type: Array, + optional: true, + items: { + type: String, + description: "IS-IS interface", + format: &ApiStringFormat::PropertyString(&IsisInterfaceProperties::API_SCHEMA), + } + }, + } +)] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +pub struct IsisNodeProperties { + /// Interfaces for this node + #[serde(default)] + pub(crate) interfaces: Vec>, +} + +impl IsisNodeProperties { + /// Returns an iterator over all the interfaces. + pub fn interfaces(&self) -> impl Iterator { + self.interfaces + .iter() + .map(|property_string| property_string.deref()) + } + + /// Returns an iterator over all the interfaces (mutable). + pub fn interfaces_mut(&mut self) -> impl Iterator { + self.interfaces + .iter_mut() + .map(|property_string| property_string.deref_mut()) + } +} + +impl Validatable for NodeSection { + type Error = FabricConfigError; + + /// Validates the [`FabricSection`]. + /// + /// Checks if we have either an IPv4 or an IPv6 address. If neither is set, return an error. + fn validate(&self) -> Result<(), Self::Error> { + if self.ip().is_none() && self.ip6().is_none() { + return Err(FabricConfigError::NodeNoIp(self.id().to_string())); + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum IsisNodeDeletableProperties { + Interfaces, +} + +/// Properties for an IS-IS interface +#[api] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +pub struct IsisInterfaceProperties { + pub(crate) name: InterfaceName, + + /// The multiplier for the hello holding time on a given interface. The range is 2 to + /// 100. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) hello_multiplier: Option, + + /// If ip and ip6 are unset, then this is an point-to-point interface + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) ip: Option, + + /// If ip6 and ip are unset, then this is an point-to-point interface + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) ip6: Option, +} + +impl IsisInterfaceProperties { + /// Get the name of the interface. + pub fn name(&self) -> &InterfaceName { + &self.name + } + + /// Set the name of the interface. + pub fn set_name(&mut self, name: InterfaceName) { + self.name = name + } + + /// Get the IPv4 of the interface. + pub fn ip(&self) -> Option { + self.ip + } + + /// Get the IPv6 of the interface. + pub fn ip6(&self) -> Option { + self.ip6 + } +} diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs index c7adf0f648e3..f22146aec9c2 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs @@ -1,4 +1,5 @@ pub mod bgp; +pub mod isis; pub mod openfabric; pub mod ospf; pub mod wireguard; -- 2.47.3