From: Gabriel Goller <g.goller@proxmox.com>
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 [thread overview]
Message-ID: <20260828113255.176546-3-g.goller@proxmox.com> (raw)
In-Reply-To: <20260828113255.176546-1-g.goller@proxmox.com>
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 <g.goller@proxmox.com>
---
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<IsisInterface>, 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<IsisInterface>, 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<OpenfabricProperties, OpenfabricNodeProperties>),
+ Isis(Entry<IsisProperties, IsisNodeProperties>),
Ospf(Entry<OspfProperties, OspfNodeProperties>),
WireGuard(Entry<WireGuardProperties, WireGuardNode>),
Bgp(Entry<BgpProperties, BgpNode>),
@@ -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::<IsisNodePropertiesUpdater, IsisNodeDeletableProperties> {
+ 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::<OspfNodePropertiesUpdater, OspfNodeDeletableProperties> {
ip,
@@ -495,6 +536,7 @@ impl FabricEntry {
pub fn nodes(&self) -> impl Iterator<Item = (&NodeId, &Node)> + '_ {
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<Node, FabricConfigError> {
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<Node>) {
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<Fabric> 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::<IsisPropertiesUpdater, IsisDeletableProperties> {
+ 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::<OspfPropertiesUpdater, OspfDeletableProperties> {
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<OpenfabricProperties> {
type Updater = FabricSectionUpdater<OpenfabricPropertiesUpdater, OpenfabricDeletableProperties>;
}
+impl UpdaterType for FabricSection<IsisProperties> {
+ type Updater = FabricSectionUpdater<IsisPropertiesUpdater, IsisDeletableProperties>;
+}
+
impl UpdaterType for FabricSection<OspfProperties> {
type Updater = FabricSectionUpdater<OspfPropertiesUpdater, OspfDeletableProperties>;
}
@@ -173,6 +180,7 @@ impl UpdaterType for FabricSection<BgpProperties> {
#[serde(rename_all = "snake_case", tag = "protocol")]
pub enum Fabric {
Openfabric(FabricSection<OpenfabricProperties>),
+ Isis(FabricSection<IsisProperties>),
Ospf(FabricSection<OspfProperties>),
#[serde(rename = "wireguard")]
WireGuard(FabricSection<WireGuardProperties>),
@@ -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<Ipv4Cidr> {
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<Ipv6Cidr> {
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<FabricSection<OpenfabricProperties>> for Fabric {
}
}
+impl From<FabricSection<IsisProperties>> for Fabric {
+ fn from(section: FabricSection<IsisProperties>) -> Self {
+ Fabric::Isis(section)
+ }
+}
+
impl From<FabricSection<OspfProperties>> for Fabric {
fn from(section: FabricSection<OspfProperties>) -> Self {
Fabric::Ospf(section)
@@ -288,6 +308,7 @@ impl From<FabricSection<BgpProperties>> for Fabric {
#[serde(rename_all = "snake_case", tag = "protocol")]
pub enum FabricUpdater {
Openfabric(<FabricSection<OpenfabricProperties> as UpdaterType>::Updater),
+ Isis(<FabricSection<IsisProperties> as UpdaterType>::Updater),
Ospf(<FabricSection<OspfProperties> as UpdaterType>::Updater),
#[serde(rename = "wireguard")]
WireGuard(<FabricSection<WireGuardProperties> 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<Section> for FabricOrNode<Fabric, Node> {
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<OpenfabricProperties>),
+ IsisFabric(FabricSection<IsisProperties>),
OspfFabric(FabricSection<OspfProperties>),
#[serde(rename = "wireguard_fabric")]
WireGuardFabric(FabricSection<WireGuardProperties>),
BgpFabric(FabricSection<BgpProperties>),
OpenfabricNode(NodeSection<OpenfabricNodeProperties>),
+ IsisNode(NodeSection<IsisNodeProperties>),
OspfNode(NodeSection<OspfNodeProperties>),
#[serde(rename = "wireguard_node")]
WireGuardNode(NodeSection<WireGuardNode>),
@@ -85,6 +90,12 @@ impl From<FabricSection<OpenfabricProperties>> for Section {
}
}
+impl From<FabricSection<IsisProperties>> for Section {
+ fn from(section: FabricSection<IsisProperties>) -> Self {
+ Self::IsisFabric(section)
+ }
+}
+
impl From<FabricSection<OspfProperties>> for Section {
fn from(section: FabricSection<OspfProperties>) -> Self {
Self::OspfFabric(section)
@@ -109,6 +120,12 @@ impl From<NodeSection<OpenfabricNodeProperties>> for Section {
}
}
+impl From<NodeSection<IsisNodeProperties>> for Section {
+ fn from(section: NodeSection<IsisNodeProperties>) -> Self {
+ Self::IsisNode(section)
+ }
+}
+
impl From<NodeSection<OspfNodeProperties>> for Section {
fn from(section: NodeSection<OspfNodeProperties>) -> Self {
Self::OspfNode(section)
@@ -131,6 +148,7 @@ impl From<Fabric> 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<Node> 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<T: ApiType> ApiType for NodeSection<T> {
#[serde(rename_all = "snake_case", tag = "protocol")]
pub enum Node {
Openfabric(NodeSection<OpenfabricNodeProperties>),
+ Isis(NodeSection<IsisNodeProperties>),
Ospf(NodeSection<OspfNodeProperties>),
#[serde(rename = "wireguard")]
WireGuard(NodeSection<WireGuardNode>),
@@ -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<std::net::Ipv4Addr> {
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<std::net::Ipv6Addr> {
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<NodeSection<OpenfabricNodeProperties>> for Node {
}
}
+impl From<NodeSection<IsisNodeProperties>> for Node {
+ fn from(value: NodeSection<IsisNodeProperties>) -> Self {
+ Self::Isis(value)
+ }
+}
+
impl From<NodeSection<OspfNodeProperties>> for Node {
fn from(value: NodeSection<OspfNodeProperties>) -> 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<OpenfabricNodeProperties>),
+ Isis(NodeData<IsisNodeProperties>),
Ospf(NodeData<OspfNodeProperties>),
#[serde(rename = "wireguard")]
WireGuard(NodeData<WireGuardNode>),
@@ -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<OpenfabricNodePropertiesUpdater, OpenfabricNodeDeletableProperties>;
}
+ impl UpdaterType for NodeData<IsisNodeProperties> {
+ type Updater = NodeDataUpdater<IsisNodePropertiesUpdater, IsisNodeDeletableProperties>;
+ }
+
impl UpdaterType for NodeData<OspfNodeProperties> {
type Updater = NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>;
}
@@ -427,6 +447,7 @@ pub mod api {
Openfabric(
NodeDataUpdater<OpenfabricNodePropertiesUpdater, OpenfabricNodeDeletableProperties>,
),
+ Isis(NodeDataUpdater<IsisNodePropertiesUpdater, IsisNodeDeletableProperties>),
Ospf(NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>),
#[serde(rename = "wireguard")]
WireGuard(NodeDataUpdater<WireGuardNodeUpdater, WireGuardNodeDeletableProperties>),
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<HelloInterval>,
+
+ /// 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<CsnpInterval>,
+
+ /// 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<PrefixListId>,
+}
+
+impl Validatable for FabricSection<IsisProperties> {
+ type Error = FabricConfigError;
+
+ /// Validates the [`FabricSection<IsisProperties>`].
+ ///
+ /// 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<PropertyString<IsisInterfaceProperties>>,
+}
+
+impl IsisNodeProperties {
+ /// Returns an iterator over all the interfaces.
+ pub fn interfaces(&self) -> impl Iterator<Item = &IsisInterfaceProperties> {
+ 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<Item = &mut IsisInterfaceProperties> {
+ self.interfaces
+ .iter_mut()
+ .map(|property_string| property_string.deref_mut())
+ }
+}
+
+impl Validatable for NodeSection<IsisNodeProperties> {
+ type Error = FabricConfigError;
+
+ /// Validates the [`FabricSection<IsisProperties>`].
+ ///
+ /// 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<HelloMultiplier>,
+
+ /// 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<Ipv4Cidr>,
+
+ /// 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<Ipv6Cidr>,
+}
+
+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<Ipv4Cidr> {
+ self.ip
+ }
+
+ /// Get the IPv6 of the interface.
+ pub fn ip6(&self) -> Option<Ipv6Cidr> {
+ 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
next prev parent reply other threads:[~2026-08-28 11:35 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-28 11:32 [PATCH docs/gui-tests/manager/network/proxmox{-ve-rs,-perl-rs} v3 00/13] Add IS-IS protocol to fabrics Gabriel Goller
2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 01/13] frr: add fabric properties to ISIS types and rename domain Gabriel Goller
2026-08-28 11:32 ` Gabriel Goller [this message]
2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 03/13] ve-config: add integration tests for IS-IS fabrics Gabriel Goller
2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 04/13] ve-config: add IS-IS status deserialization types Gabriel Goller
2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 05/13] frr: accept legacy ISIS domain field Gabriel Goller
2026-08-28 11:32 ` [PATCH proxmox-perl-rs v3 06/13] pve-rs: fabrics: add IS-IS protocol ifupdown config generation Gabriel Goller
2026-08-28 11:32 ` [PATCH proxmox-perl-rs v3 07/13] sdn: add IS-IS fabric status reporting Gabriel Goller
2026-08-28 11:32 ` [PATCH pve-network v3 08/13] fabrics: add IS-IS api types Gabriel Goller
2026-08-28 11:32 ` [PATCH pve-network v3 09/13] sdn: controllers: rename isis domain to fabric_id Gabriel Goller
2026-08-28 11:32 ` [PATCH pve-manager v3 10/13] fabrics: add IS-IS panels Gabriel Goller
2026-08-28 11:32 ` [PATCH pve-manager v3 11/13] sdn: add warning about IS-IS controller deprecation Gabriel Goller
2026-08-28 11:32 ` [PATCH pve-docs v3 12/13] sdn: add section about IS-IS fabric Gabriel Goller
2026-08-28 11:32 ` [PATCH pve-gui-tests v3 13/13] fabrics: add screenshots for IS-IS fabric and nodes Gabriel Goller
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260828113255.176546-3-g.goller@proxmox.com \
--to=g.goller@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.