all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH docs/gui-tests/manager/network/proxmox{-ve-rs,-perl-rs} v3 00/13] Add IS-IS protocol to fabrics
@ 2026-08-28 11:32 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
                   ` (12 more replies)
  0 siblings, 13 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

As part of our effort to deprecate the existing controllers and transition
toward a fabric-based architecture, we are introducing an IS-IS fabric. To
centralize all routing protocols, we plan to consolidate them under the
"Fabrics" sidebar menu. Additionally, we aim to generate all routing protocol
configurations in a unified location using Rust.

Following the integration of OpenFabric and OSPF, the next logical step is to
migrate the IS-IS controller to the fabrics framework.

This patch series adds an IS-IS Fabric option to the "Fabrics" menu while
retaining the original IS-IS controller for backward compatibility. Since the
two do not interfere, we will include a warning recommending the use of the new
IS-IS Fabrics instead. The primary difference between the IS-IS Controller and
the IS-IS Fabric is that the Fabric does not automatically redistribute
connected routes into the Link State DB. This change is intentional, as we
intend to implement a more advanced redistribution and route-map mechanism in
the future, rather than relying on a simple checkbox. Users who need to
redistribute connected routes with the new IS-IS Fabrics can do so by manually
adding the appropriate FRR statement to the /etc/frr/frr.conf.local file.

Changelog:
v2:
    * rebase ontop of master
v1: 
    * rebase ontop of template series


proxmox-ve-rs:

Gabriel Goller (5):
  frr: add fabric properties to ISIS types and rename domain
  ve-config: add IS-IS fabric config parsing and frr config generation
  ve-config: add integration tests for IS-IS fabrics
  ve-config: add IS-IS status deserialization types
  frr: accept legacy ISIS domain field

 proxmox-frr-templates/templates/isisd.jinja   |  23 +-
 proxmox-frr/src/de/isis.rs                    | 103 ++++++++
 proxmox-frr/src/de/mod.rs                     |   1 +
 proxmox-frr/src/ser/isis.rs                   |  28 ++-
 proxmox-frr/src/ser/mod.rs                    |  11 +
 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 +
 .../tests/fabric/cfg/isis_default/fabrics.cfg |  18 ++
 .../fabric/cfg/isis_dualstack/fabrics.cfg     |  22 ++
 .../fabric/cfg/isis_ipv6_only/fabrics.cfg     |  18 ++
 proxmox-ve-config/tests/fabric/main.rs        |  63 +++++
 .../snapshots/fabric__isis_default_pve.snap   |  36 +++
 .../snapshots/fabric__isis_default_pve1.snap  |  35 +++
 .../snapshots/fabric__isis_dualstack_pve.snap |  48 ++++
 .../snapshots/fabric__isis_ipv6_only_pve.snap |  37 +++
 20 files changed, 992 insertions(+), 6 deletions(-)
 create mode 100644 proxmox-frr/src/de/isis.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/isis.rs
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/isis_default/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/isis_dualstack/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/isis_ipv6_only/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__isis_default_pve.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__isis_default_pve1.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__isis_dualstack_pve.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__isis_ipv6_only_pve.snap


proxmox-perl-rs:

Gabriel Goller (2):
  pve-rs: fabrics: add IS-IS protocol ifupdown config generation
  sdn: add IS-IS fabric status reporting

 pve-rs/src/bindings/sdn/fabrics.rs | 136 +++++
 pve-rs/src/sdn/status.rs           | 949 +++++++++++++++++++++++++++--
 2 files changed, 1026 insertions(+), 59 deletions(-)


pve-network:

Gabriel Goller (2):
  fabrics: add IS-IS api types
  sdn: controllers: rename isis domain to fabric_id

 src/PVE/Network/SDN/Controllers/IsisPlugin.pm |  2 +-
 src/PVE/Network/SDN/Fabrics.pm                | 51 ++++++++++++++++---
 src/PVE/Network/SDN/Frr.pm                    |  1 +
 3 files changed, 45 insertions(+), 9 deletions(-)


pve-manager:

Gabriel Goller (2):
  fabrics: add IS-IS panels
  sdn: add warning about IS-IS controller deprecation

 www/manager6/Makefile                         |  3 ++
 www/manager6/sdn/FabricsView.js               | 12 +++++
 www/manager6/sdn/controllers/IsisEdit.js      |  5 ++
 www/manager6/sdn/fabrics/NodeEdit.js          |  1 +
 www/manager6/sdn/fabrics/isis/FabricEdit.js   | 48 +++++++++++++++++++
 .../sdn/fabrics/isis/InterfacePanel.js        | 21 ++++++++
 www/manager6/sdn/fabrics/isis/NodeEdit.js     |  9 ++++
 7 files changed, 99 insertions(+)
 create mode 100644 www/manager6/sdn/fabrics/isis/FabricEdit.js
 create mode 100644 www/manager6/sdn/fabrics/isis/InterfacePanel.js
 create mode 100644 www/manager6/sdn/fabrics/isis/NodeEdit.js


pve-docs:

Gabriel Goller (1):
  sdn: add section about IS-IS fabric

 pvesdn.adoc | 111 +++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 105 insertions(+), 6 deletions(-)


pve-gui-tests:

Gabriel Goller (1):
  fabrics: add screenshots for IS-IS fabric and nodes

 pve/fabrics-screenshots | 47 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 47 insertions(+)


Summary over all repositories:
  34 files changed, 2314 insertions(+), 80 deletions(-)

-- 
Generated by murpp 0.11.0




^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH proxmox-ve-rs v3 01/13] frr: add fabric properties to ISIS types and rename domain
  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 ` Gabriel Goller
  2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 02/13] ve-config: add IS-IS fabric config parsing and frr config generation Gabriel Goller
                   ` (11 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

The ISIS types in proxmox-frr are used by pve-network to generate FRR
configuration for ISIS controllers. To share these types between ISIS
fabric and controller implementations, add properties required by fabrics
and rename the "domain" field to "fabric_id" for consistency with fabric
terminology. This rename is also applied in pve-network.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-frr-templates/templates/isisd.jinja | 23 +++++++++++++++---
 proxmox-frr/src/ser/isis.rs                 | 27 ++++++++++++++++++++-
 proxmox-frr/src/ser/mod.rs                  | 11 +++++++++
 3 files changed, 56 insertions(+), 5 deletions(-)

diff --git a/proxmox-frr-templates/templates/isisd.jinja b/proxmox-frr-templates/templates/isisd.jinja
index e3c9292d575c..a6dd748163b8 100644
--- a/proxmox-frr-templates/templates/isisd.jinja
+++ b/proxmox-frr-templates/templates/isisd.jinja
@@ -21,11 +21,26 @@ exit
 {% endfor %}
 {% for interface_name, interface_config in isis.interfaces|items %}
 {% call interface(interface_name, interface_config.addresses_v4, interface_config.addresses_v6) %}
-{% if interface_config.domain and interface_config.is_ipv4 %}
- ip router isis {{ interface_config.domain }}
+{% if interface_config.fabric_id and interface_config.is_ipv4 %}
+ ip router isis {{ interface_config.fabric_id }}
 {% endif %}
-{% if interface_config.domain and interface_config.is_ipv6 %}
- ipv6 router isis {{ interface_config.domain }}
+{% if interface_config.fabric_id and interface_config.is_ipv6 %}
+ ipv6 router isis {{ interface_config.fabric_id }}
+{% endif %}
+{% if interface_config.passive %}
+ isis passive
+{% endif %}
+{% if interface_config.hello_interval %}
+ isis hello-interval {{ interface_config.hello_interval }}
+{% endif %}
+{% if interface_config.csnp_interval %}
+ isis csnp-interval {{ interface_config.csnp_interval }}
+{% endif %}
+{% if interface_config.hello_multiplier %}
+ isis hello-multiplier {{ interface_config.hello_multiplier }}
+{% endif %}
+{% if interface_config.point_to_point %}
+ isis network point-to-point
 {% endif %}
 {% for line in interface_config.custom_frr_config %}
 {{ line }}
diff --git a/proxmox-frr/src/ser/isis.rs b/proxmox-frr/src/ser/isis.rs
index 211c5b21e9e1..6ac30e714113 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 From<FrrWord> for IsisRouterName {
+    fn from(value: FrrWord) -> Self {
+        Self(value)
+    }
+}
+
+impl IsisRouterName {
+    pub fn new(name: FrrWord) -> Self {
+        Self(name)
+    }
+}
+
 #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
 pub enum IsisLevel {
     #[serde(rename = "level-1")]
@@ -37,11 +49,24 @@ pub struct IsisRouter {
 
 #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
 pub struct IsisInterface {
-    pub domain: IsisRouterName,
+    pub fabric_id: IsisRouterName,
     #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
     pub is_ipv4: bool,
     #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
     pub is_ipv6: bool,
+    #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    pub passive: Option<bool>,
+    // Note: openfabric is very similar to isis, so we can use the same properties here
+    #[serde(default)]
+    pub hello_interval: Option<proxmox_sdn_types::openfabric::HelloInterval>,
+    #[serde(default)]
+    pub csnp_interval: Option<proxmox_sdn_types::openfabric::CsnpInterval>,
+    #[serde(default)]
+    pub hello_multiplier: Option<proxmox_sdn_types::openfabric::HelloMultiplier>,
+
+    #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    pub point_to_point: bool,
+
     #[serde(default)]
     pub custom_frr_config: Vec<String>,
 }
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
index b651121b4a82..290e14042491 100644
--- a/proxmox-frr/src/ser/mod.rs
+++ b/proxmox-frr/src/ser/mod.rs
@@ -160,6 +160,16 @@ impl From<openfabric::OpenfabricInterface> for Interface<openfabric::OpenfabricI
     }
 }
 
+impl From<isis::IsisInterface> for Interface<isis::IsisInterface> {
+    fn from(value: isis::IsisInterface) -> Self {
+        Interface {
+            addresses_v4: Vec::new(),
+            addresses_v6: Vec::new(),
+            properties: value,
+        }
+    }
+}
+
 impl From<ospf::OspfInterface> for Interface<ospf::OspfInterface> {
     fn from(value: ospf::OspfInterface) -> Self {
         Interface {
@@ -191,6 +201,7 @@ pub struct IpRoute {
 pub enum FrrProtocol {
     Ospf,
     Openfabric,
+    Isis,
     Bgp,
 }
 
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH proxmox-ve-rs v3 02/13] ve-config: add IS-IS fabric config parsing and frr config generation
  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
  2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 03/13] ve-config: add integration tests for IS-IS fabrics Gabriel Goller
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

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(&current_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(&current_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





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH proxmox-ve-rs v3 03/13] ve-config: add integration tests for IS-IS fabrics
  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 ` [PATCH proxmox-ve-rs v3 02/13] ve-config: add IS-IS fabric config parsing and frr config generation Gabriel Goller
@ 2026-08-28 11:32 ` Gabriel Goller
  2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 04/13] ve-config: add IS-IS status deserialization types Gabriel Goller
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

Add three integration tests for the IS-IS fabrics: a simple one, one
with dualstack ipv6 and ipv4, and one with ipv6 only.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 .../tests/fabric/cfg/isis_default/fabrics.cfg | 18 ++++++
 .../fabric/cfg/isis_dualstack/fabrics.cfg     | 22 +++++++
 .../fabric/cfg/isis_ipv6_only/fabrics.cfg     | 18 ++++++
 proxmox-ve-config/tests/fabric/main.rs        | 63 +++++++++++++++++++
 .../snapshots/fabric__isis_default_pve.snap   | 36 +++++++++++
 .../snapshots/fabric__isis_default_pve1.snap  | 35 +++++++++++
 .../snapshots/fabric__isis_dualstack_pve.snap | 48 ++++++++++++++
 .../snapshots/fabric__isis_ipv6_only_pve.snap | 37 +++++++++++
 8 files changed, 277 insertions(+)
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/isis_default/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/isis_dualstack/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/isis_ipv6_only/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__isis_default_pve.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__isis_default_pve1.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__isis_dualstack_pve.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__isis_ipv6_only_pve.snap

diff --git a/proxmox-ve-config/tests/fabric/cfg/isis_default/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/isis_default/fabrics.cfg
new file mode 100644
index 000000000000..161831977bc6
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/isis_default/fabrics.cfg
@@ -0,0 +1,18 @@
+isis_fabric: uwu
+        hello_interval 4
+        ip_prefix 192.168.2.0/24
+
+isis_node: uwu_pve
+        interfaces name=ens20,hello_multiplier=50
+        interfaces name=ens19
+        ip 192.168.2.8
+
+isis_node: uwu_pve1
+        interfaces name=ens19
+        interfaces name=ens20
+        ip 192.168.2.9
+
+isis_node: uwu_pve2
+        interfaces name=ens19
+        interfaces name=ens20
+        ip 192.168.2.10
diff --git a/proxmox-ve-config/tests/fabric/cfg/isis_dualstack/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/isis_dualstack/fabrics.cfg
new file mode 100644
index 000000000000..c8a0458d2e88
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/isis_dualstack/fabrics.cfg
@@ -0,0 +1,22 @@
+isis_fabric: uwu
+        hello_interval 4
+        ip_prefix 192.168.2.0/24
+        ip6_prefix 2001:db8::0/64
+
+isis_node: uwu_pve
+        interfaces name=ens20,hello_multiplier=50
+        interfaces name=ens19
+        ip 192.168.2.8
+        ip6 2001:db8::1
+
+isis_node: uwu_pve1
+        interfaces name=ens19
+        interfaces name=ens20
+        ip 192.168.2.9
+        ip6 2001:db8::2
+
+isis_node: uwu_pve2
+        interfaces name=ens19
+        interfaces name=ens20
+        ip 192.168.2.10
+        ip6 2001:db8::3
diff --git a/proxmox-ve-config/tests/fabric/cfg/isis_ipv6_only/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/isis_ipv6_only/fabrics.cfg
new file mode 100644
index 000000000000..584ecbbc4ef9
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/isis_ipv6_only/fabrics.cfg
@@ -0,0 +1,18 @@
+isis_fabric: uwu
+        hello_interval 4
+        ip6_prefix a:b::0/75
+
+isis_node: uwu_pve
+        interfaces name=ens20,hello_multiplier=50
+        interfaces name=ens19
+        ip6 a:b::a
+
+isis_node: uwu_pve1
+        interfaces name=ens19
+        interfaces name=ens20
+        ip6 a:b::b
+
+isis_node: uwu_pve2
+        interfaces name=ens19
+        interfaces name=ens20
+        ip6 a:b::c
diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs
index ee59e9a2ea44..5332c5c4ad86 100644
--- a/proxmox-ve-config/tests/fabric/main.rs
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -45,6 +45,35 @@ fn openfabric_default() {
     insta::assert_snapshot!(helper::reference_name!("pve1"), output);
 }
 
+#[test]
+fn isis_default() {
+    let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+
+    let mut frr_config = FrrConfig::default();
+    build_fabric(
+        NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+        config.clone(),
+        &mut frr_config,
+    )
+    .unwrap();
+
+    let mut output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve"), output);
+
+    frr_config = FrrConfig::default();
+    build_fabric(
+        NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
+        config.clone(),
+        &mut frr_config,
+    )
+    .unwrap();
+
+    output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve1"), output);
+}
+
 #[test]
 fn ospf_default() {
     let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
@@ -148,6 +177,23 @@ fn openfabric_dualstack() {
     insta::assert_snapshot!(helper::reference_name!("pve"), output);
 }
 
+#[test]
+fn isis_dualstack() {
+    let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+
+    let mut frr_config = FrrConfig::default();
+    build_fabric(
+        NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+        config,
+        &mut frr_config,
+    )
+    .unwrap();
+
+    let output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve"), output);
+}
+
 #[test]
 fn openfabric_ipv6_only() {
     let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
@@ -279,3 +325,20 @@ fn bgp_merge_with_evpn() {
 
     insta::assert_snapshot!(helper::reference_name!("pve"), output);
 }
+
+#[test]
+fn isis_ipv6_only() {
+    let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+
+    let mut frr_config = FrrConfig::default();
+    build_fabric(
+        NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+        config,
+        &mut frr_config,
+    )
+    .unwrap();
+
+    let output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve"), output);
+}
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_default_pve.snap
new file mode 100644
index 000000000000..101c005e1725
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_default_pve.snap
@@ -0,0 +1,36 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+!
+router isis uwu
+ net 49.0001.1921.6800.2008.00
+exit
+!
+interface dummy_uwu
+ ip router isis uwu
+ isis passive
+exit
+!
+interface ens19
+ ip router isis uwu
+ isis hello-interval 4
+ isis network point-to-point
+exit
+!
+interface ens20
+ ip router isis uwu
+ isis hello-interval 4
+ isis hello-multiplier 50
+ isis network point-to-point
+exit
+!
+access-list pve_isis_uwu_ips permit 192.168.2.0/24
+!
+route-map pve_isis permit 100
+ match ip address pve_isis_uwu_ips
+ set src 192.168.2.8
+exit
+!
+ip protocol isis route-map pve_isis
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_default_pve1.snap
new file mode 100644
index 000000000000..c647a288e47b
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_default_pve1.snap
@@ -0,0 +1,35 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+!
+router isis uwu
+ net 49.0001.1921.6800.2009.00
+exit
+!
+interface dummy_uwu
+ ip router isis uwu
+ isis passive
+exit
+!
+interface ens19
+ ip router isis uwu
+ isis hello-interval 4
+ isis network point-to-point
+exit
+!
+interface ens20
+ ip router isis uwu
+ isis hello-interval 4
+ isis network point-to-point
+exit
+!
+access-list pve_isis_uwu_ips permit 192.168.2.0/24
+!
+route-map pve_isis permit 100
+ match ip address pve_isis_uwu_ips
+ set src 192.168.2.9
+exit
+!
+ip protocol isis route-map pve_isis
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_dualstack_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_dualstack_pve.snap
new file mode 100644
index 000000000000..c470f0393ed5
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_dualstack_pve.snap
@@ -0,0 +1,48 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+!
+router isis uwu
+ net 49.0001.1921.6800.2008.00
+exit
+!
+interface dummy_uwu
+ ip router isis uwu
+ ipv6 router isis uwu
+ isis passive
+exit
+!
+interface ens19
+ ip router isis uwu
+ ipv6 router isis uwu
+ isis hello-interval 4
+ isis network point-to-point
+exit
+!
+interface ens20
+ ip router isis uwu
+ ipv6 router isis uwu
+ isis hello-interval 4
+ isis hello-multiplier 50
+ isis network point-to-point
+exit
+!
+ipv6 access-list pve_isis_uwu_ip6s permit 2001:db8::/64
+!
+access-list pve_isis_uwu_ips permit 192.168.2.0/24
+!
+route-map pve_isis permit 100
+ match ip address pve_isis_uwu_ips
+ set src 192.168.2.8
+exit
+!
+route-map pve_isis6 permit 110
+ match ipv6 address pve_isis_uwu_ip6s
+ set src 2001:db8::1
+exit
+!
+ip protocol isis route-map pve_isis
+!
+ipv6 protocol isis route-map pve_isis6
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_ipv6_only_pve.snap
new file mode 100644
index 000000000000..802ce7fa137d
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__isis_ipv6_only_pve.snap
@@ -0,0 +1,37 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+!
+router isis uwu
+ net 49.0001.0000.0000.000a.00
+exit
+!
+interface dummy_uwu
+ ipv6 router isis uwu
+ isis passive
+exit
+!
+interface ens19
+ ipv6 router isis uwu
+ isis hello-interval 4
+ isis network point-to-point
+exit
+!
+interface ens20
+ ipv6 router isis uwu
+ isis hello-interval 4
+ isis hello-multiplier 50
+ isis network point-to-point
+exit
+!
+ipv6 access-list pve_isis_uwu_ip6s permit a:b::/75
+!
+route-map pve_isis6 permit 100
+ match ipv6 address pve_isis_uwu_ip6s
+ set src a:b::a
+exit
+!
+!
+ipv6 protocol isis route-map pve_isis6
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH proxmox-ve-rs v3 04/13] ve-config: add IS-IS status deserialization types
  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
                   ` (2 preceding siblings ...)
  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 ` Gabriel Goller
  2026-08-28 11:32 ` [PATCH proxmox-ve-rs v3 05/13] frr: accept legacy ISIS domain field Gabriel Goller
                   ` (8 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

Add types to deserialize the FRR json output when getting the status of
a fabric.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-frr/src/de/isis.rs | 103 +++++++++++++++++++++++++++++++++++++
 proxmox-frr/src/de/mod.rs  |   1 +
 2 files changed, 104 insertions(+)
 create mode 100644 proxmox-frr/src/de/isis.rs

diff --git a/proxmox-frr/src/de/isis.rs b/proxmox-frr/src/de/isis.rs
new file mode 100644
index 000000000000..46c4357125a8
--- /dev/null
+++ b/proxmox-frr/src/de/isis.rs
@@ -0,0 +1,103 @@
+use serde::{Deserialize, Serialize};
+
+/// State of the adjacency of a Isis neighbor
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+pub enum AdjacencyState {
+    Initializing,
+    Up,
+    Down,
+    #[serde(other)]
+    Unknown,
+}
+
+/// Neighbor Interface
+///
+/// Interface used to communicate with a specific neighbor
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct NeighborInterface {
+    /// The name of the interface
+    pub name: String,
+    /// The state of the adjacency, this is "Up" when everything is well
+    pub state: Option<AdjacencyState>,
+    /// Time since the last adj-flap (basically the uptime)
+    #[serde(rename = "last-ago")]
+    pub last_ago: String,
+}
+
+/// Adjacency information
+///
+/// Circuits are Layer-2 Broadcast domains (Either point-to-point or LAN).
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct Circuit {
+    /// The hostname of the adjacency peer
+    pub adj: Option<String>,
+    /// The interface of the neighbor
+    pub interface: Option<NeighborInterface>,
+}
+
+/// An isis area the same as SDN fabric.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct Area {
+    /// The are name, this is the same as the fabric_id, so the name of the fabric.
+    pub area: String,
+    /// Circuits are Layer-2 Broadcast domains (Either point-to-point or LAN).
+    pub circuits: Vec<Circuit>,
+}
+
+/// The parsed neighbors.
+///
+/// This models the output of:
+/// `vtysh -c 'show isis neighbor json'`.
+#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
+pub struct Neighbors {
+    /// Every sdn fabric is also an isis 'area'
+    pub areas: Vec<Area>,
+}
+
+/// The NetworkType of a isis interface
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+pub enum NetworkType {
+    #[serde(rename(deserialize = "p2p", serialize = "Point-To-Point"))]
+    PointToPoint,
+    #[serde(rename(deserialize = "lan", serialize = "Broadcast"))]
+    Lan,
+    #[serde(rename(deserialize = "loopback", serialize = "Loopback"))]
+    Loopback,
+    #[serde(rename = "Unknown", other)]
+    Unknown,
+}
+
+/// The State of a isis interface
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+pub enum CircuitState {
+    Init,
+    Config,
+    Up,
+    #[serde(other)]
+    Unknown,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "kebab-case")]
+pub struct Interface {
+    pub name: String,
+    pub state: CircuitState,
+    #[serde(rename = "type")]
+    pub ty: NetworkType,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct InterfaceCircuits {
+    pub interface: Interface,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
+pub struct InterfaceArea {
+    pub area: String,
+    pub circuits: Vec<InterfaceCircuits>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
+pub struct Interfaces {
+    pub areas: Vec<InterfaceArea>,
+}
diff --git a/proxmox-frr/src/de/mod.rs b/proxmox-frr/src/de/mod.rs
index 3f2bd68d9cec..d5ba35b3c7a6 100644
--- a/proxmox-frr/src/de/mod.rs
+++ b/proxmox-frr/src/de/mod.rs
@@ -4,6 +4,7 @@ use proxmox_network_types::ip_address::Cidr;
 use serde::{Deserialize, Serialize};
 
 pub mod evpn;
+pub mod isis;
 pub mod openfabric;
 pub mod ospf;
 
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH proxmox-ve-rs v3 05/13] frr: accept legacy ISIS domain field
  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
                   ` (3 preceding siblings ...)
  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 ` 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
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

Accept "domain" as an alias for "fabric_id" when deserializing ISIS
interface configuration. This keeps older producers compatible while new
configuration continues to serialize with the "fabric_id" field.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-frr/src/ser/isis.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/proxmox-frr/src/ser/isis.rs b/proxmox-frr/src/ser/isis.rs
index 6ac30e714113..b6e221937ec8 100644
--- a/proxmox-frr/src/ser/isis.rs
+++ b/proxmox-frr/src/ser/isis.rs
@@ -49,6 +49,7 @@ pub struct IsisRouter {
 
 #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
 pub struct IsisInterface {
+    #[serde(alias = "domain")]
     pub fabric_id: IsisRouterName,
     #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
     pub is_ipv4: bool,
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH proxmox-perl-rs v3 06/13] pve-rs: fabrics: add IS-IS protocol ifupdown config generation
  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
                   ` (4 preceding siblings ...)
  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 ` Gabriel Goller
  2026-08-28 11:32 ` [PATCH proxmox-perl-rs v3 07/13] sdn: add IS-IS fabric status reporting Gabriel Goller
                   ` (6 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

Generate the interfaces that have been configured with IS-IS to the
ifupdown config (enable ip-forwarding on all). IS-IS needs to have the
ip-address duplicated from the dummy interface to every interface in
unnumbered mode (like with ospf). This could potentially be fixed in the
future with [0].

[0]: https://github.com/FRRouting/frr/pull/19429
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pve-rs/src/bindings/sdn/fabrics.rs | 44 ++++++++++++++++++++++++++++++
 1 file changed, 44 insertions(+)

diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index f96b6b1c656f..33ed92cfd033 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -400,6 +400,13 @@ pub mod pve_rs_sdn_fabrics {
                         }
                     }
                 }
+                ConfigNode::Isis(node_section) => {
+                    for interface in node_section.properties_mut().interfaces_mut() {
+                        if let Some(mapped_name) = map_name(&mapping, interface.name())? {
+                            interface.set_name(mapped_name);
+                        }
+                    }
+                }
                 ConfigNode::Ospf(node_section) => {
                     for interface in node_section.properties_mut().interfaces_mut() {
                         if let Some(mapped_name) = map_name(&mapping, interface.name())? {
@@ -513,6 +520,9 @@ pub mod pve_rs_sdn_fabrics {
                 FabricEntry::Bgp(_) => {
                     daemons.insert("bgpd");
                 }
+                FabricEntry::Isis(_) => {
+                    daemons.insert("isisd");
+                }
             };
         }
 
@@ -681,6 +691,40 @@ pub mod pve_rs_sdn_fabrics {
                         }
                     }
                 }
+                ConfigNode::Isis(node_section) => {
+                    for interface in node_section.properties().interfaces() {
+                        if let Some(ip) = interface.ip() {
+                            let interface =
+                                render_interface(interface.name(), Cidr::from(ip), None)?;
+                            writeln!(interfaces)?;
+                            write!(interfaces, "{interface}")?;
+                        }
+                        if let Some(ip) = interface.ip6() {
+                            let interface =
+                                render_interface(interface.name(), Cidr::from(ip), None)?;
+                            writeln!(interfaces)?;
+                            write!(interfaces, "{interface}")?;
+                        }
+
+                        // If no ip is configured, add auto and iface with node ip to bring
+                        // interface up ISIS doesn't really need an ip on the interface, but the
+                        // problem is that arp can't tell which source address to use in some
+                        // cases, so it's better if we set the node address on all the fabric
+                        // interfaces.
+                        if let (None, None) = (interface.ip(), interface.ip6()) {
+                            let cidr = Cidr::from(if let Some(ip) = node.ip() {
+                                IpAddr::from(ip)
+                            } else if let Some(ip) = node.ip6() {
+                                IpAddr::from(ip)
+                            } else {
+                                anyhow::bail!("there has to be a ipv4 or ipv6 node address");
+                            });
+                            let interface = render_interface(interface.name(), cidr, None)?;
+                            writeln!(interfaces)?;
+                            write!(interfaces, "{interface}")?;
+                        }
+                    }
+                }
                 ConfigNode::Ospf(node_section) => {
                     for interface in node_section.properties().interfaces() {
                         writeln!(interfaces)?;
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH proxmox-perl-rs v3 07/13] sdn: add IS-IS fabric status reporting
  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
                   ` (5 preceding siblings ...)
  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 ` Gabriel Goller
  2026-08-28 11:32 ` [PATCH pve-network v3 08/13] fabrics: add IS-IS api types Gabriel Goller
                   ` (5 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

Extend fabric status reporting to cover the IS-IS fabric alongside the
existing OpenFabric and OSPF support.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pve-rs/src/bindings/sdn/fabrics.rs |  92 +++
 pve-rs/src/sdn/status.rs           | 949 +++++++++++++++++++++++++++--
 2 files changed, 982 insertions(+), 59 deletions(-)

diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 33ed92cfd033..ffc5c7fb580e 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -891,6 +891,36 @@ pub mod pve_rs_sdn_fabrics {
                     proxmox_sys::nodename(),
                 )
             }
+            FabricEntry::Isis(_) => {
+                let isis_ipv4_routes_string = String::from_utf8(
+                    Command::new("sh")
+                        .args(["-c", "vtysh -c 'show ip route isis json'"])
+                        .output()?
+                        .stdout,
+                )?;
+
+                let isis_ipv6_routes_string = String::from_utf8(
+                    Command::new("sh")
+                        .args(["-c", "vtysh -c 'show ipv6 route isis json'"])
+                        .output()?
+                        .stdout,
+                )?;
+
+                let mut isis_routes: proxmox_frr::de::Routes = if isis_ipv4_routes_string.is_empty()
+                {
+                    proxmox_frr::de::Routes::default()
+                } else {
+                    serde_json::from_str(&isis_ipv4_routes_string)
+                        .with_context(|| "error parsing isis ipv4 routes")?
+                };
+                if !isis_ipv6_routes_string.is_empty() {
+                    let isis_ipv6_routes: proxmox_frr::de::Routes =
+                        serde_json::from_str(&isis_ipv6_routes_string)
+                            .with_context(|| "error parsing isis ipv6 routes")?;
+                    isis_routes.0.extend(isis_ipv6_routes.0);
+                }
+                status::get_routes(fabric_id, config, isis_routes, proxmox_sys::nodename())
+            }
             FabricEntry::Ospf(_) => {
                 let ospf_routes_string = String::from_utf8(
                     Command::new("sh")
@@ -974,6 +1004,23 @@ pub mod pve_rs_sdn_fabrics {
 
                 status::get_neighbors_openfabric(fabric_id, openfabric_neighbors).map(|v| v.into())
             }
+            FabricEntry::Isis(_) => {
+                let isis_neighbors_string = String::from_utf8(
+                    Command::new("sh")
+                        .args(["-c", "vtysh -c 'show isis neighbor detail json'"])
+                        .output()?
+                        .stdout,
+                )?;
+                let isis_neighbors: proxmox_frr::de::isis::Neighbors =
+                    if isis_neighbors_string.is_empty() {
+                        proxmox_frr::de::isis::Neighbors::default()
+                    } else {
+                        serde_json::from_str(&isis_neighbors_string)
+                            .with_context(|| "error parsing isis neighbors")?
+                    };
+
+                status::get_neighbors_isis(fabric_id, isis_neighbors).map(|v| v.into())
+            }
             FabricEntry::Ospf(fabric) => {
                 let ospf_neighbors_string = String::from_utf8(
                     Command::new("sh")
@@ -1052,6 +1099,23 @@ pub mod pve_rs_sdn_fabrics {
                 status::get_interfaces_openfabric(fabric_id, openfabric_interfaces)
                     .map(|v| v.into())
             }
+            FabricEntry::Isis(_) => {
+                let isis_interface_string = String::from_utf8(
+                    Command::new("sh")
+                        .args(["-c", "vtysh -c 'show isis interface json'"])
+                        .output()?
+                        .stdout,
+                )?;
+                let isis_interfaces: proxmox_frr::de::isis::Interfaces =
+                    if isis_interface_string.is_empty() {
+                        proxmox_frr::de::isis::Interfaces::default()
+                    } else {
+                        serde_json::from_str(&isis_interface_string)
+                            .with_context(|| "error parsing isis interfaces")?
+                    };
+
+                status::get_interfaces_isis(fabric_id, isis_interfaces).map(|v| v.into())
+            }
             FabricEntry::Ospf(fabric) => {
                 let ospf_interfaces_string = String::from_utf8(
                     Command::new("sh")
@@ -1126,6 +1190,20 @@ pub mod pve_rs_sdn_fabrics {
                 .stdout,
         )?;
 
+        let isis_ipv4_routes_string = String::from_utf8(
+            Command::new("sh")
+                .args(["-c", "vtysh -c 'show ip route isis json'"])
+                .output()?
+                .stdout,
+        )?;
+
+        let isis_ipv6_routes_string = String::from_utf8(
+            Command::new("sh")
+                .args(["-c", "vtysh -c 'show ipv6 route isis json'"])
+                .output()?
+                .stdout,
+        )?;
+
         let ospf_routes_string = String::from_utf8(
             Command::new("sh")
                 .args(["-c", "vtysh -c 'show ip route ospf json'"])
@@ -1147,6 +1225,19 @@ pub mod pve_rs_sdn_fabrics {
             openfabric_routes.0.extend(openfabric_ipv6_routes.0);
         }
 
+        let mut isis_routes: proxmox_frr::de::Routes = if isis_ipv4_routes_string.is_empty() {
+            proxmox_frr::de::Routes::default()
+        } else {
+            serde_json::from_str(&isis_ipv4_routes_string)
+                .with_context(|| "error parsing isis ipv4 routes")?
+        };
+        if !isis_ipv6_routes_string.is_empty() {
+            let isis_ipv6_routes: proxmox_frr::de::Routes =
+                serde_json::from_str(&isis_ipv6_routes_string)
+                    .with_context(|| "error parsing isis ipv6 routes")?;
+            isis_routes.0.extend(isis_ipv6_routes.0);
+        }
+
         let ospf_routes: proxmox_frr::de::Routes = if ospf_routes_string.is_empty() {
             proxmox_frr::de::Routes::default()
         } else {
@@ -1187,6 +1278,7 @@ pub mod pve_rs_sdn_fabrics {
             openfabric: openfabric_routes,
             ospf: ospf_routes,
             bgp: bgp_routes,
+            isis: isis_routes,
         };
 
         status::get_status(config, route_status, proxmox_sys::nodename())
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index 7a1334d20804..b43bc953a87d 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -81,6 +81,33 @@ mod openfabric {
     }
 }
 
+mod isis {
+    use proxmox_frr::de;
+    use serde::Serialize;
+
+    /// The status of a neighbor.
+    ///
+    /// Contains the neighbor name and the neighbor status.
+    #[derive(Debug, Serialize, PartialEq, Eq)]
+    pub struct NeighborStatus {
+        pub neighbor: String,
+        pub status: de::isis::AdjacencyState,
+        pub uptime: String,
+    }
+
+    /// The status of a fabric interface
+    ///
+    /// Contains the interface name, the interface state (so if the interface is up/down) and the type
+    /// of the interface (e.g. point-to-point, broadcast, etc.).
+    #[derive(Debug, Serialize, PartialEq, Eq)]
+    pub struct InterfaceStatus {
+        pub name: String,
+        pub state: de::isis::CircuitState,
+        #[serde(rename = "type")]
+        pub ty: de::isis::NetworkType,
+    }
+}
+
 mod wireguard {
     use serde::Serialize;
 
@@ -117,6 +144,7 @@ pub enum NeighborStatus {
     Ospf(Vec<ospf::NeighborStatus>),
     WireGuard(Vec<wireguard::NeighborStatus>),
     Bgp(Vec<bgp::NeighborStatus>),
+    Isis(Vec<isis::NeighborStatus>),
 }
 
 impl From<Vec<openfabric::NeighborStatus>> for NeighborStatus {
@@ -134,6 +162,11 @@ impl From<Vec<bgp::NeighborStatus>> for NeighborStatus {
         NeighborStatus::Bgp(value)
     }
 }
+impl From<Vec<isis::NeighborStatus>> for NeighborStatus {
+    fn from(value: Vec<isis::NeighborStatus>) -> Self {
+        NeighborStatus::Isis(value)
+    }
+}
 
 /// Common InterfaceStatus that contains either OSPF, Openfabric, or BGP interfaces
 #[derive(Debug, Serialize)]
@@ -143,6 +176,7 @@ pub enum InterfaceStatus {
     Ospf(Vec<ospf::InterfaceStatus>),
     WireGuard(Vec<wireguard::InterfaceStatus>),
     Bgp(Vec<bgp::InterfaceStatus>),
+    Isis(Vec<isis::InterfaceStatus>),
 }
 
 impl From<Vec<openfabric::InterfaceStatus>> for InterfaceStatus {
@@ -160,6 +194,11 @@ impl From<Vec<bgp::InterfaceStatus>> for InterfaceStatus {
         InterfaceStatus::Bgp(value)
     }
 }
+impl From<Vec<isis::InterfaceStatus>> for InterfaceStatus {
+    fn from(value: Vec<isis::InterfaceStatus>) -> Self {
+        InterfaceStatus::Isis(value)
+    }
+}
 
 /// The status of a route.
 ///
@@ -182,6 +221,8 @@ pub enum Protocol {
     WireGuard,
     /// BGP
     Bgp,
+    /// IS-IS
+    Isis,
 }
 
 /// The status of a fabric.
@@ -222,6 +263,8 @@ pub struct RoutesParsed {
     pub ospf: de::Routes,
     /// All bgp routes in FRR
     pub bgp: de::Routes,
+    /// All isis routes in FRR
+    pub isis: de::Routes,
 }
 
 /// Config used to parse the fabric part of the running-config
@@ -271,6 +314,11 @@ pub fn get_routes(
                 BgpNode::Internal(props) => props.interfaces().map(|i| i.name().as_str()).collect(),
                 BgpNode::External(_) => HashSet::new(),
             },
+            ConfigNode::Isis(n) => n
+                .properties()
+                .interfaces()
+                .map(|i| i.name().as_str())
+                .collect(),
         };
 
         let dummy_interface = format!("dummy_{}", fabric_id.as_str());
@@ -355,6 +403,38 @@ pub fn get_neighbors_openfabric(
     Ok(stats)
 }
 
+/// Convert the parsed isis neighbor neighbor information into a list of
+/// [`isis::NeighborStatus`].
+///
+/// ISIS uses the name of the fabric as an "area", so simply match that to the fabric_id.
+pub fn get_neighbors_isis(
+    fabric_id: FabricId,
+    neighbors: de::isis::Neighbors,
+) -> Result<Vec<isis::NeighborStatus>, anyhow::Error> {
+    let mut stats: Vec<isis::NeighborStatus> = Vec::new();
+
+    for area in &neighbors.areas {
+        if area.area != fabric_id.as_str() {
+            continue;
+        }
+        for circuit in &area.circuits {
+            let (Some(adj), Some(interface)) = (&circuit.adj, &circuit.interface) else {
+                continue;
+            };
+            let Some(state) = interface.state else {
+                continue;
+            };
+            stats.push(isis::NeighborStatus {
+                neighbor: adj.clone(),
+                status: state,
+                uptime: interface.last_ago.clone(),
+            });
+        }
+    }
+
+    Ok(stats)
+}
+
 /// Convert the parsed ospf neighbor neighbor information into a list of [`ospf::NeighborStatus`].
 ///
 /// Ospf does not use the name of the fabric at all, so we again need to retrieve the interfaces of
@@ -421,6 +501,30 @@ pub fn get_interfaces_openfabric(
     Ok(stats)
 }
 
+/// Conver the `show isis interface` output into a list of [`isis::InterfaceStatus`].
+///
+/// isis uses the name of the fabric as an "area", so simply match that to the fabric_id.
+pub fn get_interfaces_isis(
+    fabric_id: FabricId,
+    interfaces: de::isis::Interfaces,
+) -> Result<Vec<isis::InterfaceStatus>, anyhow::Error> {
+    let mut stats: Vec<isis::InterfaceStatus> = Vec::new();
+
+    for area in &interfaces.areas {
+        if area.area == fabric_id.as_str() {
+            for circuit in &area.circuits {
+                stats.push(isis::InterfaceStatus {
+                    name: circuit.interface.name.clone(),
+                    state: circuit.interface.state,
+                    ty: circuit.interface.ty,
+                });
+            }
+        }
+    }
+
+    Ok(stats)
+}
+
 /// Convert the `show ip ospf interface` output into a list of [`ospf::InterfaceStatus`].
 ///
 /// Ospf does not use the name of the fabric at all, so we again need to retrieve the interfaces of
@@ -541,6 +645,7 @@ pub fn get_status(
             ConfigNode::Ospf(_) => (Protocol::Ospf, &routes.ospf.0),
             ConfigNode::WireGuard(_) => (Protocol::WireGuard, &BTreeMap::new()),
             ConfigNode::Bgp(_) => (Protocol::Bgp, &routes.bgp.0),
+            ConfigNode::Isis(_) => (Protocol::Isis, &routes.isis.0),
         };
 
         // get interfaces
@@ -560,6 +665,11 @@ pub fn get_status(
                 BgpNode::Internal(props) => props.interfaces().map(|i| i.name().as_str()).collect(),
                 BgpNode::External(_) => HashSet::new(),
             },
+            ConfigNode::Isis(n) => n
+                .properties()
+                .interfaces()
+                .map(|i| i.name().as_str())
+                .collect(),
         };
 
         // determine status by checking if any routes exist for our interfaces
@@ -580,6 +690,7 @@ pub fn get_status(
             Protocol::Ospf if has_routes => FabricStatus::Ok,
             Protocol::Bgp if has_routes => FabricStatus::Ok,
             Protocol::WireGuard => FabricStatus::Ok,
+            Protocol::Isis if has_routes => FabricStatus::Ok,
             _ => FabricStatus::NotOk,
         };
 
@@ -825,7 +936,530 @@ mod tests {
             .expect("error converting section config to fabricconfig")
     }
 
-    mod openfabric {
+    mod openfabric {
+        use super::super::*;
+
+        #[test]
+        fn neighbors() {
+            let json_output = r#"
+                {
+                  "areas":[
+                    {
+                      "area":"test",
+                      "circuits":[
+                        {
+                          "circuit":0
+                        },
+                        {
+                          "circuit":0,
+                          "adj":"node2",
+                          "interface":{
+                            "name":"ens19",
+                            "state":"Up",
+                            "adj-flaps":1,
+                            "last-ago":"11m5s",
+                            "circuit-type":"L2",
+                            "speaks":"IPv4",
+                            "snpa":"2020.2020.2020",
+                            "area-address":{
+                              "isonet":"49.0001"
+                            },
+                            "ipv4-address":{
+                              "ipv4":"172.16.6.2"
+                            },
+                            "adj-sid":{}
+                          },
+                          "level":2,
+                          "expires-in":"29s"
+                        }
+                      ]
+                    }
+                  ]
+                }
+                "#;
+
+            let neighbors: de::openfabric::Neighbors = if json_output.is_empty() {
+                de::openfabric::Neighbors::default()
+            } else {
+                serde_json::from_str(json_output).expect("error parsing json output")
+            };
+
+            let output = get_neighbors_openfabric(
+                FabricId::from_string("test".to_owned()).expect("error parsing fabricId"),
+                neighbors,
+            )
+            .expect("error converting vtysh output");
+
+            let reference = vec![openfabric::NeighborStatus {
+                neighbor: "node2".to_owned(),
+                status: de::openfabric::AdjacencyState::Up,
+                uptime: "11m5s".to_owned(),
+            }];
+            assert_eq!(reference, output);
+        }
+
+        #[test]
+        fn multiple_neighbors() {
+            let json_output = r#"
+            {
+              "areas":[
+                {
+                  "area":"test",
+                  "circuits":[
+                    {
+                      "circuit":0
+                    },
+                    {
+                      "circuit":0,
+                      "adj":"node1",
+                      "interface":{
+                        "name":"ens19",
+                        "state":"Up",
+                        "adj-flaps":1,
+                        "last-ago":"25m26s",
+                        "circuit-type":"L2",
+                        "speaks":"IPv4",
+                        "snpa":"2020.2020.2020",
+                        "area-address":{
+                          "isonet":"49.0001"
+                        },
+                        "ipv4-address":{
+                          "ipv4":"172.16.6.1"
+                        },
+                        "adj-sid":{}
+                      },
+                      "level":2,
+                      "expires-in":"28s"
+                    },
+                    {
+                      "circuit":0,
+                      "adj":"node3",
+                      "interface":{
+                        "name":"ens20",
+                        "state":"Up",
+                        "adj-flaps":1,
+                        "last-ago":"25m21s",
+                        "circuit-type":"L2",
+                        "speaks":"IPv4",
+                        "snpa":"2020.2020.2020",
+                        "area-address":{
+                          "isonet":"49.0001"
+                        },
+                        "ipv4-address":{
+                          "ipv4":"172.16.6.3"
+                        },
+                        "adj-sid":{}
+                      },
+                      "level":2,
+                      "expires-in":"29s"
+                    }
+                  ]
+                }
+              ]
+            }
+            "#;
+
+            let neighbors: de::openfabric::Neighbors = if json_output.is_empty() {
+                de::openfabric::Neighbors::default()
+            } else {
+                serde_json::from_str(json_output).expect("error parsing json output")
+            };
+
+            let output = get_neighbors_openfabric(
+                FabricId::from_string("test".to_owned()).expect("error parsing fabricId"),
+                neighbors,
+            )
+            .expect("error converting vtysh output");
+
+            let reference = vec![
+                openfabric::NeighborStatus {
+                    neighbor: "node1".to_owned(),
+                    status: de::openfabric::AdjacencyState::Up,
+                    uptime: "25m26s".to_owned(),
+                },
+                openfabric::NeighborStatus {
+                    neighbor: "node3".to_owned(),
+                    status: de::openfabric::AdjacencyState::Up,
+                    uptime: "25m21s".to_owned(),
+                },
+            ];
+            assert_eq!(reference, output);
+        }
+
+        #[test]
+        fn multiple_neighbors_multiple_areas() {
+            let json_output = r#"
+            {
+              "areas":[
+                {
+                  "area":"test",
+                  "circuits":[
+                    {
+                      "circuit":0
+                    },
+                    {
+                      "circuit":0,
+                      "adj":"node1",
+                      "interface":{
+                        "name":"ens19",
+                        "state":"Up",
+                        "adj-flaps":1,
+                        "last-ago":"33m39s",
+                        "circuit-type":"L2",
+                        "speaks":"IPv4",
+                        "snpa":"2020.2020.2020",
+                        "area-address":{
+                          "isonet":"49.0001"
+                        },
+                        "ipv4-address":{
+                          "ipv4":"172.16.6.1"
+                        },
+                        "adj-sid":{}
+                      },
+                      "level":2,
+                      "expires-in":"29s"
+                    },
+                    {
+                      "circuit":0,
+                      "adj":"node3",
+                      "interface":{
+                        "name":"ens20",
+                        "state":"Up",
+                        "adj-flaps":1,
+                        "last-ago":"33m34s",
+                        "circuit-type":"L2",
+                        "speaks":"IPv4",
+                        "snpa":"2020.2020.2020",
+                        "area-address":{
+                          "isonet":"49.0001"
+                        },
+                        "ipv4-address":{
+                          "ipv4":"172.16.6.3"
+                        },
+                        "adj-sid":{}
+                      },
+                      "level":2,
+                      "expires-in":"29s"
+                    }
+                  ]
+                },
+                {
+                  "area":"test1",
+                  "circuits":[
+                    {
+                      "circuit":0
+                    },
+                    {
+                      "circuit":0,
+                      "adj":"node1",
+                      "interface":{
+                        "name":"ens21",
+                        "state":"Up",
+                        "adj-flaps":1,
+                        "last-ago":"56s",
+                        "circuit-type":"L2",
+                        "speaks":"IPv4",
+                        "snpa":"2020.2020.2020",
+                        "area-address":{
+                          "isonet":"49.0001"
+                        },
+                        "ipv4-address":{
+                          "ipv4":"172.16.7.1"
+                        },
+                        "adj-sid":{}
+                      },
+                      "level":2,
+                      "expires-in":"28s"
+                    },
+                    {
+                      "circuit":0,
+                      "adj":"node3",
+                      "interface":{
+                        "name":"ens22",
+                        "state":"Up",
+                        "adj-flaps":1,
+                        "last-ago":"1m2s",
+                        "circuit-type":"L2",
+                        "speaks":"IPv4",
+                        "snpa":"2020.2020.2020",
+                        "area-address":{
+                          "isonet":"49.0001"
+                        },
+                        "ipv4-address":{
+                          "ipv4":"172.16.7.3"
+                        },
+                        "adj-sid":{}
+                      },
+                      "level":2,
+                      "expires-in":"28s"
+                    }
+                  ]
+                }
+              ]
+            }
+            "#;
+
+            let neighbors: de::openfabric::Neighbors = if json_output.is_empty() {
+                de::openfabric::Neighbors::default()
+            } else {
+                serde_json::from_str(json_output).expect("error parsing json output")
+            };
+
+            let output_node1 = get_neighbors_openfabric(
+                FabricId::from_string("test".to_owned()).expect("error parsing fabricId"),
+                neighbors.clone(),
+            )
+            .expect("error converting vtysh output");
+
+            let reference_node1 = vec![
+                openfabric::NeighborStatus {
+                    neighbor: "node1".to_owned(),
+                    status: de::openfabric::AdjacencyState::Up,
+                    uptime: "33m39s".to_owned(),
+                },
+                openfabric::NeighborStatus {
+                    neighbor: "node3".to_owned(),
+                    status: de::openfabric::AdjacencyState::Up,
+                    uptime: "33m34s".to_owned(),
+                },
+            ];
+            assert_eq!(reference_node1, output_node1);
+
+            let output_node2 = get_neighbors_openfabric(
+                FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"),
+                neighbors,
+            )
+            .expect("error converting vtysh output");
+
+            let reference_node2 = vec![
+                openfabric::NeighborStatus {
+                    neighbor: "node1".to_owned(),
+                    status: de::openfabric::AdjacencyState::Up,
+                    uptime: "56s".to_owned(),
+                },
+                openfabric::NeighborStatus {
+                    neighbor: "node3".to_owned(),
+                    status: de::openfabric::AdjacencyState::Up,
+                    uptime: "1m2s".to_owned(),
+                },
+            ];
+            assert_eq!(reference_node2, output_node2);
+        }
+
+        #[test]
+        fn interfaces() {
+            let json_output = r#"
+            {
+              "areas":[
+                {
+                  "area":"test1",
+                  "circuits":[
+                    {
+                      "circuit":0,
+                      "interface":{
+                        "name":"dummy_test1",
+                        "circuit-id":"0x0",
+                        "state":"Up",
+                        "type":"loopback",
+                        "level":"L2"
+                      }
+                    },
+                    {
+                      "circuit":0,
+                      "interface":{
+                        "name":"ens21",
+                        "circuit-id":"0x0",
+                        "state":"Up",
+                        "type":"p2p",
+                        "level":"L2"
+                      }
+                    },
+                    {
+                      "circuit":0,
+                      "interface":{
+                        "name":"ens22",
+                        "circuit-id":"0x0",
+                        "state":"Up",
+                        "type":"p2p",
+                        "level":"L2"
+                      }
+                    }
+                  ]
+                }
+              ]
+            }
+            "#;
+
+            let interfaces: de::openfabric::Interfaces = if json_output.is_empty() {
+                de::openfabric::Interfaces::default()
+            } else {
+                serde_json::from_str(json_output).expect("error parsing json output")
+            };
+
+            let output = get_interfaces_openfabric(
+                FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"),
+                interfaces,
+            )
+            .expect("error converting vtysh output");
+
+            let reference = vec![
+                openfabric::InterfaceStatus {
+                    name: "dummy_test1".to_owned(),
+                    state: de::openfabric::CircuitState::Up,
+                    ty: de::openfabric::NetworkType::Loopback,
+                },
+                openfabric::InterfaceStatus {
+                    name: "ens21".to_owned(),
+                    state: de::openfabric::CircuitState::Up,
+                    ty: de::openfabric::NetworkType::PointToPoint,
+                },
+                openfabric::InterfaceStatus {
+                    name: "ens22".to_owned(),
+                    state: de::openfabric::CircuitState::Up,
+                    ty: de::openfabric::NetworkType::PointToPoint,
+                },
+            ];
+            assert_eq!(reference, output);
+        }
+
+        #[test]
+        fn interfaces_multiple_areas() {
+            let json_output = r#"
+            {
+              "areas":[
+                {
+                  "area":"test",
+                  "circuits":[
+                    {
+                      "circuit":0,
+                      "interface":{
+                        "name":"dummy_test",
+                        "circuit-id":"0x0",
+                        "state":"Up",
+                        "type":"loopback",
+                        "level":"L2"
+                      }
+                    },
+                    {
+                      "circuit":0,
+                      "interface":{
+                        "name":"ens19",
+                        "circuit-id":"0x0",
+                        "state":"Up",
+                        "type":"p2p",
+                        "level":"L2"
+                      }
+                    },
+                    {
+                      "circuit":0,
+                      "interface":{
+                        "name":"ens20",
+                        "circuit-id":"0x0",
+                        "state":"Up",
+                        "type":"p2p",
+                        "level":"L2"
+                      }
+                    }
+                  ]
+                },
+                {
+                  "area":"test1",
+                  "circuits":[
+                    {
+                      "circuit":0,
+                      "interface":{
+                        "name":"dummy_test1",
+                        "circuit-id":"0x0",
+                        "state":"Up",
+                        "type":"loopback",
+                        "level":"L2"
+                      }
+                    },
+                    {
+                      "circuit":0,
+                      "interface":{
+                        "name":"ens21",
+                        "circuit-id":"0x0",
+                        "state":"Up",
+                        "type":"p2p",
+                        "level":"L2"
+                      }
+                    },
+                    {
+                      "circuit":0,
+                      "interface":{
+                        "name":"ens22",
+                        "circuit-id":"0x0",
+                        "state":"Up",
+                        "type":"p2p",
+                        "level":"L2"
+                      }
+                    }
+                  ]
+                }
+              ]
+            }
+            "#;
+
+            let interfaces: de::openfabric::Interfaces = if json_output.is_empty() {
+                de::openfabric::Interfaces::default()
+            } else {
+                serde_json::from_str(json_output).expect("error parsing json output")
+            };
+
+            let output_fabric1 = get_interfaces_openfabric(
+                FabricId::from_string("test".to_owned()).expect("error parsing fabricId"),
+                interfaces.clone(),
+            )
+            .expect("error converting vtysh output");
+
+            let reference_fabric1 = vec![
+                openfabric::InterfaceStatus {
+                    name: "dummy_test".to_owned(),
+                    state: de::openfabric::CircuitState::Up,
+                    ty: de::openfabric::NetworkType::Loopback,
+                },
+                openfabric::InterfaceStatus {
+                    name: "ens19".to_owned(),
+                    state: de::openfabric::CircuitState::Up,
+                    ty: de::openfabric::NetworkType::PointToPoint,
+                },
+                openfabric::InterfaceStatus {
+                    name: "ens20".to_owned(),
+                    state: de::openfabric::CircuitState::Up,
+                    ty: de::openfabric::NetworkType::PointToPoint,
+                },
+            ];
+            assert_eq!(reference_fabric1, output_fabric1);
+
+            let output_fabric2 = get_interfaces_openfabric(
+                FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"),
+                interfaces,
+            )
+            .expect("error converting vtysh output");
+
+            let reference_fabric2 = vec![
+                openfabric::InterfaceStatus {
+                    name: "dummy_test1".to_owned(),
+                    state: de::openfabric::CircuitState::Up,
+                    ty: de::openfabric::NetworkType::Loopback,
+                },
+                openfabric::InterfaceStatus {
+                    name: "ens21".to_owned(),
+                    state: de::openfabric::CircuitState::Up,
+                    ty: de::openfabric::NetworkType::PointToPoint,
+                },
+                openfabric::InterfaceStatus {
+                    name: "ens22".to_owned(),
+                    state: de::openfabric::CircuitState::Up,
+                    ty: de::openfabric::NetworkType::PointToPoint,
+                },
+            ];
+            assert_eq!(reference_fabric2, output_fabric2);
+        }
+    }
+
+    mod isis {
         use super::super::*;
 
         #[test]
@@ -867,21 +1501,21 @@ mod tests {
                 }
                 "#;
 
-            let neighbors: de::openfabric::Neighbors = if json_output.is_empty() {
-                de::openfabric::Neighbors::default()
+            let neighbors: de::isis::Neighbors = if json_output.is_empty() {
+                de::isis::Neighbors::default()
             } else {
                 serde_json::from_str(json_output).expect("error parsing json output")
             };
 
-            let output = get_neighbors_openfabric(
+            let output = get_neighbors_isis(
                 FabricId::from_string("test".to_owned()).expect("error parsing fabricId"),
                 neighbors,
             )
             .expect("error converting vtysh output");
 
-            let reference = vec![openfabric::NeighborStatus {
+            let reference = vec![isis::NeighborStatus {
                 neighbor: "node2".to_owned(),
-                status: de::openfabric::AdjacencyState::Up,
+                status: de::isis::AdjacencyState::Up,
                 uptime: "11m5s".to_owned(),
             }];
             assert_eq!(reference, output);
@@ -948,27 +1582,27 @@ mod tests {
             }
             "#;
 
-            let neighbors: de::openfabric::Neighbors = if json_output.is_empty() {
-                de::openfabric::Neighbors::default()
+            let neighbors: de::isis::Neighbors = if json_output.is_empty() {
+                de::isis::Neighbors::default()
             } else {
                 serde_json::from_str(json_output).expect("error parsing json output")
             };
 
-            let output = get_neighbors_openfabric(
+            let output = get_neighbors_isis(
                 FabricId::from_string("test".to_owned()).expect("error parsing fabricId"),
                 neighbors,
             )
             .expect("error converting vtysh output");
 
             let reference = vec![
-                openfabric::NeighborStatus {
+                isis::NeighborStatus {
                     neighbor: "node1".to_owned(),
-                    status: de::openfabric::AdjacencyState::Up,
+                    status: de::isis::AdjacencyState::Up,
                     uptime: "25m26s".to_owned(),
                 },
-                openfabric::NeighborStatus {
+                isis::NeighborStatus {
                     neighbor: "node3".to_owned(),
-                    status: de::openfabric::AdjacencyState::Up,
+                    status: de::isis::AdjacencyState::Up,
                     uptime: "25m21s".to_owned(),
                 },
             ];
@@ -1088,47 +1722,47 @@ mod tests {
             }
             "#;
 
-            let neighbors: de::openfabric::Neighbors = if json_output.is_empty() {
-                de::openfabric::Neighbors::default()
+            let neighbors: de::isis::Neighbors = if json_output.is_empty() {
+                de::isis::Neighbors::default()
             } else {
                 serde_json::from_str(json_output).expect("error parsing json output")
             };
 
-            let output_node1 = get_neighbors_openfabric(
+            let output_node1 = get_neighbors_isis(
                 FabricId::from_string("test".to_owned()).expect("error parsing fabricId"),
                 neighbors.clone(),
             )
             .expect("error converting vtysh output");
 
             let reference_node1 = vec![
-                openfabric::NeighborStatus {
+                isis::NeighborStatus {
                     neighbor: "node1".to_owned(),
-                    status: de::openfabric::AdjacencyState::Up,
+                    status: de::isis::AdjacencyState::Up,
                     uptime: "33m39s".to_owned(),
                 },
-                openfabric::NeighborStatus {
+                isis::NeighborStatus {
                     neighbor: "node3".to_owned(),
-                    status: de::openfabric::AdjacencyState::Up,
+                    status: de::isis::AdjacencyState::Up,
                     uptime: "33m34s".to_owned(),
                 },
             ];
             assert_eq!(reference_node1, output_node1);
 
-            let output_node2 = get_neighbors_openfabric(
+            let output_node2 = get_neighbors_isis(
                 FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"),
                 neighbors,
             )
             .expect("error converting vtysh output");
 
             let reference_node2 = vec![
-                openfabric::NeighborStatus {
+                isis::NeighborStatus {
                     neighbor: "node1".to_owned(),
-                    status: de::openfabric::AdjacencyState::Up,
+                    status: de::isis::AdjacencyState::Up,
                     uptime: "56s".to_owned(),
                 },
-                openfabric::NeighborStatus {
+                isis::NeighborStatus {
                     neighbor: "node3".to_owned(),
-                    status: de::openfabric::AdjacencyState::Up,
+                    status: de::isis::AdjacencyState::Up,
                     uptime: "1m2s".to_owned(),
                 },
             ];
@@ -1179,33 +1813,33 @@ mod tests {
             }
             "#;
 
-            let interfaces: de::openfabric::Interfaces = if json_output.is_empty() {
-                de::openfabric::Interfaces::default()
+            let interfaces: de::isis::Interfaces = if json_output.is_empty() {
+                de::isis::Interfaces::default()
             } else {
                 serde_json::from_str(json_output).expect("error parsing json output")
             };
 
-            let output = get_interfaces_openfabric(
+            let output = get_interfaces_isis(
                 FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"),
                 interfaces,
             )
             .expect("error converting vtysh output");
 
             let reference = vec![
-                openfabric::InterfaceStatus {
+                isis::InterfaceStatus {
                     name: "dummy_test1".to_owned(),
-                    state: de::openfabric::CircuitState::Up,
-                    ty: de::openfabric::NetworkType::Loopback,
+                    state: de::isis::CircuitState::Up,
+                    ty: de::isis::NetworkType::Loopback,
                 },
-                openfabric::InterfaceStatus {
+                isis::InterfaceStatus {
                     name: "ens21".to_owned(),
-                    state: de::openfabric::CircuitState::Up,
-                    ty: de::openfabric::NetworkType::PointToPoint,
+                    state: de::isis::CircuitState::Up,
+                    ty: de::isis::NetworkType::PointToPoint,
                 },
-                openfabric::InterfaceStatus {
+                isis::InterfaceStatus {
                     name: "ens22".to_owned(),
-                    state: de::openfabric::CircuitState::Up,
-                    ty: de::openfabric::NetworkType::PointToPoint,
+                    state: de::isis::CircuitState::Up,
+                    ty: de::isis::NetworkType::PointToPoint,
                 },
             ];
             assert_eq!(reference, output);
@@ -1290,58 +1924,58 @@ mod tests {
             }
             "#;
 
-            let interfaces: de::openfabric::Interfaces = if json_output.is_empty() {
-                de::openfabric::Interfaces::default()
+            let interfaces: de::isis::Interfaces = if json_output.is_empty() {
+                de::isis::Interfaces::default()
             } else {
                 serde_json::from_str(json_output).expect("error parsing json output")
             };
 
-            let output_fabric1 = get_interfaces_openfabric(
+            let output_fabric1 = get_interfaces_isis(
                 FabricId::from_string("test".to_owned()).expect("error parsing fabricId"),
                 interfaces.clone(),
             )
             .expect("error converting vtysh output");
 
             let reference_fabric1 = vec![
-                openfabric::InterfaceStatus {
+                isis::InterfaceStatus {
                     name: "dummy_test".to_owned(),
-                    state: de::openfabric::CircuitState::Up,
-                    ty: de::openfabric::NetworkType::Loopback,
+                    state: de::isis::CircuitState::Up,
+                    ty: de::isis::NetworkType::Loopback,
                 },
-                openfabric::InterfaceStatus {
+                isis::InterfaceStatus {
                     name: "ens19".to_owned(),
-                    state: de::openfabric::CircuitState::Up,
-                    ty: de::openfabric::NetworkType::PointToPoint,
+                    state: de::isis::CircuitState::Up,
+                    ty: de::isis::NetworkType::PointToPoint,
                 },
-                openfabric::InterfaceStatus {
+                isis::InterfaceStatus {
                     name: "ens20".to_owned(),
-                    state: de::openfabric::CircuitState::Up,
-                    ty: de::openfabric::NetworkType::PointToPoint,
+                    state: de::isis::CircuitState::Up,
+                    ty: de::isis::NetworkType::PointToPoint,
                 },
             ];
             assert_eq!(reference_fabric1, output_fabric1);
 
-            let output_fabric2 = get_interfaces_openfabric(
+            let output_fabric2 = get_interfaces_isis(
                 FabricId::from_string("test1".to_owned()).expect("error parsing fabricId"),
                 interfaces,
             )
             .expect("error converting vtysh output");
 
             let reference_fabric2 = vec![
-                openfabric::InterfaceStatus {
+                isis::InterfaceStatus {
                     name: "dummy_test1".to_owned(),
-                    state: de::openfabric::CircuitState::Up,
-                    ty: de::openfabric::NetworkType::Loopback,
+                    state: de::isis::CircuitState::Up,
+                    ty: de::isis::NetworkType::Loopback,
                 },
-                openfabric::InterfaceStatus {
+                isis::InterfaceStatus {
                     name: "ens21".to_owned(),
-                    state: de::openfabric::CircuitState::Up,
-                    ty: de::openfabric::NetworkType::PointToPoint,
+                    state: de::isis::CircuitState::Up,
+                    ty: de::isis::NetworkType::PointToPoint,
                 },
-                openfabric::InterfaceStatus {
+                isis::InterfaceStatus {
                     name: "ens22".to_owned(),
-                    state: de::openfabric::CircuitState::Up,
-                    ty: de::openfabric::NetworkType::PointToPoint,
+                    state: de::isis::CircuitState::Up,
+                    ty: de::isis::NetworkType::PointToPoint,
                 },
             ];
             assert_eq!(reference_fabric2, output_fabric2);
@@ -2243,6 +2877,203 @@ mod tests {
             assert_eq!(reference_fabric2, output_fabric2);
         }
 
+        #[test]
+        fn routes_isis() {
+            let json_output = r#"
+            {
+              "172.16.6.1/32": [
+                {
+                  "prefix": "172.16.6.1/32",
+                  "prefixLen": 32,
+                  "protocol": "isis",
+                  "vrfId": 0,
+                  "vrfName": "default",
+                  "selected": true,
+                  "destSelected": true,
+                  "distance": 115,
+                  "metric": 20,
+                  "installed": true,
+                  "table": 254,
+                  "internalStatus": 16,
+                  "internalFlags": 8,
+                  "internalNextHopNum": 1,
+                  "internalNextHopActiveNum": 1,
+                  "nexthopGroupId": 74,
+                  "installedNexthopGroupId": 74,
+                  "uptime": "00:00:32",
+                  "nexthops": [
+                    {
+                      "flags": 11,
+                      "fib": true,
+                      "ip": "172.16.6.1",
+                      "afi": "ipv4",
+                      "interfaceIndex": 3,
+                      "interfaceName": "ens19",
+                      "active": true,
+                      "onLink": true,
+                      "rmapSource": "172.16.6.2",
+                      "weight": 1
+                    }
+                  ]
+                }
+              ],
+              "172.16.6.3/32": [
+                {
+                  "prefix": "172.16.6.3/32",
+                  "prefixLen": 32,
+                  "protocol": "isis",
+                  "vrfId": 0,
+                  "vrfName": "default",
+                  "selected": true,
+                  "destSelected": true,
+                  "distance": 115,
+                  "metric": 20,
+                  "installed": true,
+                  "table": 254,
+                  "internalStatus": 16,
+                  "internalFlags": 8,
+                  "internalNextHopNum": 1,
+                  "internalNextHopActiveNum": 1,
+                  "nexthopGroupId": 75,
+                  "installedNexthopGroupId": 75,
+                  "uptime": "00:00:32",
+                  "nexthops": [
+                    {
+                      "flags": 11,
+                      "fib": true,
+                      "ip": "172.16.6.3",
+                      "afi": "ipv4",
+                      "interfaceIndex": 4,
+                      "interfaceName": "ens20",
+                      "active": true,
+                      "onLink": true,
+                      "rmapSource": "172.16.6.2",
+                      "weight": 1
+                    }
+                  ]
+                }
+              ],
+              "172.16.7.1/32": [
+                {
+                  "prefix": "172.16.7.1/32",
+                  "prefixLen": 32,
+                  "protocol": "isis",
+                  "vrfId": 0,
+                  "vrfName": "default",
+                  "selected": true,
+                  "destSelected": true,
+                  "distance": 115,
+                  "metric": 20,
+                  "installed": true,
+                  "table": 254,
+                  "internalStatus": 16,
+                  "internalFlags": 8,
+                  "internalNextHopNum": 1,
+                  "internalNextHopActiveNum": 1,
+                  "nexthopGroupId": 76,
+                  "installedNexthopGroupId": 76,
+                  "uptime": "00:00:32",
+                  "nexthops": [
+                    {
+                      "flags": 11,
+                      "fib": true,
+                      "ip": "172.16.7.1",
+                      "afi": "ipv4",
+                      "interfaceIndex": 5,
+                      "interfaceName": "ens21",
+                      "active": true,
+                      "onLink": true,
+                      "rmapSource": "172.16.7.2",
+                      "weight": 1
+                    }
+                  ]
+                }
+              ],
+              "172.16.7.3/32": [
+                {
+                  "prefix": "172.16.7.3/32",
+                  "prefixLen": 32,
+                  "protocol": "isis",
+                  "vrfId": 0,
+                  "vrfName": "default",
+                  "selected": true,
+                  "destSelected": true,
+                  "distance": 115,
+                  "metric": 20,
+                  "installed": true,
+                  "table": 254,
+                  "internalStatus": 16,
+                  "internalFlags": 8,
+                  "internalNextHopNum": 1,
+                  "internalNextHopActiveNum": 1,
+                  "nexthopGroupId": 77,
+                  "installedNexthopGroupId": 77,
+                  "uptime": "00:00:32",
+                  "nexthops": [
+                    {
+                      "flags": 11,
+                      "fib": true,
+                      "ip": "172.16.7.3",
+                      "afi": "ipv4",
+                      "interfaceIndex": 6,
+                      "interfaceName": "ens22",
+                      "active": true,
+                      "onLink": true,
+                      "rmapSource": "172.16.7.2",
+                      "weight": 1
+                    }
+                  ]
+                }
+              ]
+            }
+            "#;
+
+            let routes: de::Routes = if json_output.is_empty() {
+                de::Routes::default()
+            } else {
+                serde_json::from_str(json_output).expect("error parsing json output")
+            };
+
+            let fabric_config = sample_two_fabric_config();
+
+            let fabric_id =
+                FabricId::from_string("test".to_owned()).expect("error parsing fabricId");
+
+            let output_fabric1 =
+                get_routes(fabric_id, fabric_config.clone(), routes.clone(), "node2")
+                    .expect("error converting vtysh output");
+
+            let reference_fabric1 = vec![
+                RouteStatus {
+                    route: "172.16.6.1/32".to_owned(),
+                    via: vec!["172.16.6.1".to_owned()],
+                },
+                RouteStatus {
+                    route: "172.16.6.3/32".to_owned(),
+                    via: vec!["172.16.6.3".to_owned()],
+                },
+            ];
+            assert_eq!(reference_fabric1, output_fabric1);
+
+            let fabric_id =
+                FabricId::from_string("test1".to_owned()).expect("error parsing fabricId");
+
+            let output_fabric2 = get_routes(fabric_id, fabric_config, routes, "node2")
+                .expect("error converting vtysh output");
+
+            let reference_fabric2 = vec![
+                RouteStatus {
+                    route: "172.16.7.1/32".to_owned(),
+                    via: vec!["172.16.7.1".to_owned()],
+                },
+                RouteStatus {
+                    route: "172.16.7.3/32".to_owned(),
+                    via: vec!["172.16.7.3".to_owned()],
+                },
+            ];
+            assert_eq!(reference_fabric2, output_fabric2);
+        }
+
         #[test]
         fn routes_openfabric() {
             let json_output = r#"
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pve-network v3 08/13] fabrics: add IS-IS api types
  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
                   ` (6 preceding siblings ...)
  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 ` Gabriel Goller
  2026-08-28 11:32 ` [PATCH pve-network v3 09/13] sdn: controllers: rename isis domain to fabric_id Gabriel Goller
                   ` (4 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

Add IS-IS api-types and add it to the protocol enum. Also make 'isisd' a
frr daemon managed by pve (This means we can enable and disable it).

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN/Fabrics.pm | 51 ++++++++++++++++++++++++++++------
 src/PVE/Network/SDN/Frr.pm     |  1 +
 2 files changed, 44 insertions(+), 8 deletions(-)

diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index 4f842f1013ef..7a6e936bf735 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -53,7 +53,7 @@ PVE::JSONSchema::register_standard_option(
     {
         description => "Type of configuration entry in an SDN Fabric section config",
         type => 'string',
-        enum => ['openfabric', 'ospf', 'wireguard', 'bgp'],
+        enum => ['openfabric', 'ospf', 'wireguard', 'bgp', 'isis'],
     },
 );
 
@@ -281,6 +281,41 @@ sub node_properties {
                     description => 'OpenFabric network interface',
                     optional => 1,
                 },
+                {
+                    type => 'array',
+                    'instance-types' => ['isis'],
+                    items => {
+                        type => 'string',
+                        format => {
+                            name => {
+                                type => 'string',
+                                format => 'pve-iface',
+                                description => 'Name of the network interface',
+                            },
+                            hello_multiplier => {
+                                type => 'integer',
+                                description => 'The hello_multiplier property of the interface',
+                                optional => 1,
+                                minimum => 2,
+                                maximum => 100,
+                            },
+                            ip => {
+                                type => 'string',
+                                format => 'CIDRv4',
+                                description => 'IPv4 address for this node',
+                                optional => 1,
+                            },
+                            ip6 => {
+                                type => 'string',
+                                format => 'CIDRv6',
+                                description => 'IPv6 address for this node',
+                                optional => 1,
+                            },
+                        },
+                    },
+                    description => 'IS-IS network interface',
+                    optional => 1,
+                },
                 {
                     type => 'array',
                     'instance-types' => ['ospf'],
@@ -438,7 +473,7 @@ sub node_properties {
                 },
                 {
                     type => 'array',
-                    'instance-types' => ['openfabric', 'ospf'],
+                    'instance-types' => ['openfabric', 'ospf', 'isis'],
                     items => {
                         type => 'string',
                         enum => ['interfaces', 'ip', 'ip6'],
@@ -484,8 +519,8 @@ sub fabric_properties {
         hello_interval => {
             type => 'number',
             'type-property' => 'protocol',
-            'instance-types' => ['openfabric'],
-            description => 'The hello_interval property for Openfabric',
+            'instance-types' => ['openfabric', 'isis'],
+            description => 'The hello_interval property for Openfabric and IS-IS',
             optional => 1,
             minimum => 1,
             maximum => 600,
@@ -493,8 +528,8 @@ sub fabric_properties {
         csnp_interval => {
             type => 'number',
             'type-property' => 'protocol',
-            'instance-types' => ['openfabric'],
-            description => 'The csnp_interval property for Openfabric',
+            'instance-types' => ['openfabric', 'isis'],
+            description => 'The csnp_interval property for Openfabric and IS-IS',
             optional => 1,
             minimum => 1,
             maximum => 600,
@@ -511,7 +546,7 @@ sub fabric_properties {
             type => 'string',
             format => 'pve-sdn-prefix-list-id',
             'type-property' => 'protocol',
-            'instance-types' => ['ospf', 'openfabric'],
+            'instance-types' => ['ospf', 'openfabric', 'isis'],
             description =>
                 'A prefix list that should be used for filtering routes that are to be installed into the kernel routing table',
             optional => 1,
@@ -598,7 +633,7 @@ sub fabric_properties {
             oneOf => [
                 {
                     type => 'array',
-                    'instance-types' => ['openfabric'],
+                    'instance-types' => ['openfabric', 'isis'],
                     items => {
                         type => 'string',
                         enum => [
diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
index f81da325b1ef..729feb9e6cbd 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -126,6 +126,7 @@ my $SDN_DAEMONS_DEFAULT = {
     ospfd => 0,
     fabricd => 0,
     bgpd => 1,
+    isisd => 1,
 };
 
 =head3 set_daemon_status(\%daemons, $set_default)
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pve-network v3 09/13] sdn: controllers: rename isis domain to fabric_id
  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
                   ` (7 preceding siblings ...)
  2026-08-28 11:32 ` [PATCH pve-network v3 08/13] fabrics: add IS-IS api types Gabriel Goller
@ 2026-08-28 11:32 ` Gabriel Goller
  2026-08-28 11:32 ` [PATCH pve-manager v3 10/13] fabrics: add IS-IS panels Gabriel Goller
                   ` (3 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

Rename the "domain" property to "fabric_id" in the ISIS controller to
align with the ISIS fabric naming convention and enable sharing of
proxmox-frr ISIS types between both components.

The ISIS fabric and ISIS controller share basic configuration options,
but previously used different property names ("fabric_id" vs "domain").
Since fabrics are hardcoded to use "fabric_id", standardize the
controller to use the same naming.

For this change to be backwards compatible libpve-rs-perl and
libpve-network-perl will need to be updated in sync.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN/Controllers/IsisPlugin.pm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
index 272d55efbfc7..5ddbf4d32e69 100644
--- a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
@@ -90,7 +90,7 @@ sub generate_frr_config {
     for my $iface (sort @ifaces) {
         my $iface_name = $altnames->{$iface} // $iface;
         $config->{frr}->{isis}->{interfaces}->{$iface_name} //= {};
-        $config->{frr}->{isis}->{interfaces}->{$iface_name}->{domain} = $isis_domain;
+        $config->{frr}->{isis}->{interfaces}->{$iface_name}->{fabric_id} = $isis_domain;
         $config->{frr}->{isis}->{interfaces}->{$iface_name}->{is_ipv4} = 1;
         $config->{frr}->{isis}->{interfaces}->{$iface_name}->{is_ipv6} = 0;
     }
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pve-manager v3 10/13] fabrics: add IS-IS panels
  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
                   ` (8 preceding siblings ...)
  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 ` Gabriel Goller
  2026-08-28 11:32 ` [PATCH pve-manager v3 11/13] sdn: add warning about IS-IS controller deprecation Gabriel Goller
                   ` (2 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

The IS-IS panels have the same properties as OpenFabric both on the
FabricEdit and the NodeEdit.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 www/manager6/Makefile                         |  3 ++
 www/manager6/sdn/FabricsView.js               | 12 +++++
 www/manager6/sdn/fabrics/NodeEdit.js          |  1 +
 www/manager6/sdn/fabrics/isis/FabricEdit.js   | 48 +++++++++++++++++++
 .../sdn/fabrics/isis/InterfacePanel.js        | 21 ++++++++
 www/manager6/sdn/fabrics/isis/NodeEdit.js     |  9 ++++
 6 files changed, 94 insertions(+)
 create mode 100644 www/manager6/sdn/fabrics/isis/FabricEdit.js
 create mode 100644 www/manager6/sdn/fabrics/isis/InterfacePanel.js
 create mode 100644 www/manager6/sdn/fabrics/isis/NodeEdit.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index eb0e9d9cece0..cecc2f2027f3 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -347,6 +347,9 @@ JSSRC= 							\
 	sdn/fabrics/openfabric/InterfacePanel.js	\
 	sdn/fabrics/openfabric/NodeEdit.js		\
 	sdn/fabrics/openfabric/FabricEdit.js		\
+	sdn/fabrics/isis/InterfacePanel.js		\
+	sdn/fabrics/isis/NodeEdit.js			\
+	sdn/fabrics/isis/FabricEdit.js			\
 	sdn/fabrics/ospf/InterfacePanel.js		\
 	sdn/fabrics/ospf/NodeEdit.js			\
 	sdn/fabrics/ospf/FabricEdit.js			\
diff --git a/www/manager6/sdn/FabricsView.js b/www/manager6/sdn/FabricsView.js
index 88926462df11..b8374f8a155d 100644
--- a/www/manager6/sdn/FabricsView.js
+++ b/www/manager6/sdn/FabricsView.js
@@ -32,6 +32,7 @@ Ext.define('PVE.sdn.Fabric.View', {
                 if (rec.data.type === 'fabric') {
                     const PROTOCOL_DISPLAY_NAMES = {
                         openfabric: 'OpenFabric',
+                        isis: 'IS-IS',
                         ospf: 'OSPF',
                         wireguard: 'WireGuard',
                         bgp: 'BGP',
@@ -197,6 +198,10 @@ Ext.define('PVE.sdn.Fabric.View', {
                             text: 'OpenFabric',
                             handler: 'addOpenfabric',
                         },
+                        {
+                            text: 'IS-IS',
+                            handler: 'addIsis',
+                        },
                         {
                             text: 'OSPF',
                             handler: 'addOspf',
@@ -286,6 +291,7 @@ Ext.define('PVE.sdn.Fabric.View', {
         getFabricEditPanel: function (protocol) {
             const FABRIC_PANELS = {
                 openfabric: 'PVE.sdn.Fabric.OpenFabric.Fabric.Edit',
+                isis: 'PVE.sdn.Fabric.Isis.Fabric.Edit',
                 ospf: 'PVE.sdn.Fabric.Ospf.Fabric.Edit',
                 wireguard: 'PVE.sdn.Fabric.WireGuard.Fabric.Edit',
                 bgp: 'PVE.sdn.Fabric.Bgp.Fabric.Edit',
@@ -297,6 +303,7 @@ Ext.define('PVE.sdn.Fabric.View', {
         getNodeEditPanel: function (protocol) {
             const NODE_PANELS = {
                 openfabric: 'PVE.sdn.Fabric.OpenFabric.Node.Edit',
+                isis: 'PVE.sdn.Fabric.Isis.Node.Edit',
                 ospf: 'PVE.sdn.Fabric.Ospf.Node.Edit',
                 wireguard: 'PVE.sdn.Fabric.WireGuard.Node.Edit',
                 bgp: 'PVE.sdn.Fabric.Bgp.Node.Edit',
@@ -324,6 +331,11 @@ Ext.define('PVE.sdn.Fabric.View', {
             me.openFabricAddWindow('openfabric');
         },
 
+        addIsis: function () {
+            let me = this;
+            me.openFabricAddWindow('isis');
+        },
+
         addOspf: function () {
             let me = this;
             me.openFabricAddWindow('ospf');
diff --git a/www/manager6/sdn/fabrics/NodeEdit.js b/www/manager6/sdn/fabrics/NodeEdit.js
index 0ace12cdc34b..6f19f5baff7d 100644
--- a/www/manager6/sdn/fabrics/NodeEdit.js
+++ b/www/manager6/sdn/fabrics/NodeEdit.js
@@ -264,6 +264,7 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
     getInterfacePanel: function (protocol) {
         const INTERFACE_PANELS = {
             openfabric: 'PVE.sdn.Fabric.OpenFabric.InterfacePanel',
+            isis: 'PVE.sdn.Fabric.Isis.InterfacePanel',
             ospf: 'PVE.sdn.Fabric.Ospf.InterfacePanel',
             wireguard: 'PVE.sdn.Fabric.WireGuard.InterfacePanel',
             bgp: 'PVE.sdn.Fabric.Bgp.InterfacePanel',
diff --git a/www/manager6/sdn/fabrics/isis/FabricEdit.js b/www/manager6/sdn/fabrics/isis/FabricEdit.js
new file mode 100644
index 000000000000..c9d8557dd561
--- /dev/null
+++ b/www/manager6/sdn/fabrics/isis/FabricEdit.js
@@ -0,0 +1,48 @@
+Ext.define('PVE.sdn.Fabric.Isis.Fabric.Edit', {
+    extend: 'PVE.sdn.Fabric.Fabric.Edit',
+
+    subject: 'IS-IS',
+    onlineHelp: 'pvesdn_isis_fabric',
+
+    hasIpv6Support: true,
+
+    extraRequestParams: {
+        protocol: 'isis',
+    },
+
+    additionalItems: [
+        {
+            xtype: 'proxmoxintegerfield',
+            fieldLabel: gettext('Hello Interval'),
+            labelWidth: 120,
+            name: 'hello_interval',
+            allowBlank: true,
+            emptyText: '3',
+            skipEmptyText: true,
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            },
+        },
+        {
+            xtype: 'proxmoxintegerfield',
+            fieldLabel: gettext('CSNP Interval'),
+            labelWidth: 120,
+            name: 'csnp_interval',
+            allowBlank: true,
+            emptyText: '10',
+            skipEmptyText: true,
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            },
+        },
+        {
+            xtype: 'pveSDNPrefixListSelector',
+            name: 'route_filter',
+            fieldLabel: gettext('Route Filter'),
+            labelWidth: 120,
+            emptyText: gettext('IP Prefixes'),
+            deleteEmpty: true,
+            skipEmptyText: true,
+        },
+    ],
+});
diff --git a/www/manager6/sdn/fabrics/isis/InterfacePanel.js b/www/manager6/sdn/fabrics/isis/InterfacePanel.js
new file mode 100644
index 000000000000..af49fbcb0664
--- /dev/null
+++ b/www/manager6/sdn/fabrics/isis/InterfacePanel.js
@@ -0,0 +1,21 @@
+Ext.define('PVE.sdn.Fabric.Isis.InterfacePanel', {
+    extend: 'PVE.sdn.Fabric.InterfacePanel',
+
+    additionalColumns: [
+        {
+            text: gettext('Hello Multiplier'),
+            xtype: 'widgetcolumn',
+            dataIndex: 'hello_multiplier',
+            flex: 1,
+            hidden: true,
+            widget: {
+                xtype: 'proxmoxintegerfield',
+                isFormField: false,
+                emptyText: '10',
+                bind: {
+                    disabled: '{record.isDisabled}',
+                },
+            },
+        },
+    ],
+});
diff --git a/www/manager6/sdn/fabrics/isis/NodeEdit.js b/www/manager6/sdn/fabrics/isis/NodeEdit.js
new file mode 100644
index 000000000000..b86097414265
--- /dev/null
+++ b/www/manager6/sdn/fabrics/isis/NodeEdit.js
@@ -0,0 +1,9 @@
+Ext.define('PVE.sdn.Fabric.Isis.Node.Edit', {
+    extend: 'PVE.sdn.Fabric.Node.Edit',
+    protocol: 'isis',
+    onlineHelp: 'pvesdn_isis_node',
+
+    extraRequestParams: {
+        protocol: 'isis',
+    },
+});
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pve-manager v3 11/13] sdn: add warning about IS-IS controller deprecation
  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
                   ` (9 preceding siblings ...)
  2026-08-28 11:32 ` [PATCH pve-manager v3 10/13] fabrics: add IS-IS panels Gabriel Goller
@ 2026-08-28 11:32 ` 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
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

Instead of the IS-IS controller the new IS-IS fabric should be used.
Every option in the IS-IS controller can also be set on the new IS-IS
fabric. Nevertheless they can both be used simultaniously and there is
not yet a clear roadmap of when we are going to deprecate the IS-IS
controller.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 www/manager6/sdn/controllers/IsisEdit.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/www/manager6/sdn/controllers/IsisEdit.js b/www/manager6/sdn/controllers/IsisEdit.js
index 75f6ccf1019e..e9beffef2b2f 100644
--- a/www/manager6/sdn/controllers/IsisEdit.js
+++ b/www/manager6/sdn/controllers/IsisEdit.js
@@ -20,6 +20,11 @@ Ext.define('PVE.sdn.controllers.IsisInputPanel', {
         var me = this;
 
         me.items = [
+            {
+                xtype: 'displayfield',
+                value: 'Consider using the new IS-IS fabrics in the "Fabrics" in the sidebar. The "isis" controller will be deprecated soon.',
+                userCls: 'pmx-hint',
+            },
             {
                 xtype: 'pveNodeSelector',
                 name: 'node',
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pve-docs v3 12/13] sdn: add section about IS-IS fabric
  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
                   ` (10 preceding siblings ...)
  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 ` 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
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

Add short section about IS-IS fabrics, nearly the same as the OpenFabric
section. Also add a warning on the IS-IS Controller about the
deprecation and a link to the IS-IS Fabrics. Also mention the
differences between them, which is that the fabrics don't redistribute
connected routes.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pvesdn.adoc | 111 +++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 105 insertions(+), 6 deletions(-)

diff --git a/pvesdn.adoc b/pvesdn.adoc
index 3fd3533ad07f..42c072c4c35e 100644
--- a/pvesdn.adoc
+++ b/pvesdn.adoc
@@ -579,6 +579,10 @@ bgp-multipath-as-path-relax:: Allow ECMP if your peers have different ASN.
 ISIS Controller
 ~~~~~~~~~~~~~~~
 
+WARNING: Consider using the xref:pvesdn_isis[IS-IS] Fabric, the ISIS Controller
+will be deprecated soon. Note that the IS-IS Fabric does not redistribute
+connected routes into the fabric.
+
 The ISIS controller is not used directly by a zone.
 You can use it to configure FRR to export EVPN routes to an ISIS domain.
 
@@ -673,12 +677,12 @@ behavior and proper source address selection throughout the fabric.
 Notes on IPv6
 ^^^^^^^^^^^^^
 
-IPv6 is currently only usable on OpenFabric fabrics. These IPv6 Fabrics need
-global IPv6 forwarding enabled on all nodes contained in the fabric. Without
-IPv6 forwarding, non-full-mesh fabrics won't work because the transit nodes
-don't forward packets to the outer nodes. Currently there isn't an easy way to
-enable IPv6 forwarding per-interface like with IPv4, so it has to be enabled
-globally. This can be accomplished by appending this line:
+IPv6 is currently only usable on OpenFabric and IS-IS fabrics. These IPv6
+Fabrics need global IPv6 forwarding enabled on all nodes contained in the
+fabric. Without IPv6 forwarding, non-full-mesh fabrics won't work because the
+transit nodes don't forward packets to the outer nodes. Currently there isn't
+an easy way to enable IPv6 forwarding per-interface like with IPv4, so it has
+to be enabled globally. This can be accomplished by appending this line:
 
 ----
 post-up sysctl -w net.ipv6.conf.all.forwarding=1
@@ -781,6 +785,101 @@ WARNING: When you remove an interface with an entry in `/etc/network/interfaces`
 that has `manual` set, then the IP will not get removed on applying the SDN
 configuration.
 
+
+[[pvesdn_isis]]
+IS-IS
+~~~~~
+
+NOTE: Unlike the IS-IS Controller, the IS-IS Fabric does not distribute all the
+connected routes into the fabric. So it does not set
+`redistribute ipv4/6 connected level-1`.
+
+IS-IS is a routing protocol specifically designed for data center fabrics.
+
+[thumbnail="screenshot/gui-datacenter-create-fabric-isis.png"]
+
+Configuration options:
+
+[[pvesdn_isis_fabric]]
+On the Fabric
+^^^^^^^^^^^^^
+
+Name:: This is the name (domain) of the IS-IS fabric and can be at most 8 characters long.
+
+IPv4 Prefix:: IPv4 CIDR network range (e.g., 192.0.2.0/24) used to verify that
+all router-IDs in the fabric are contained within this prefix.
+
+IPv6 Prefix:: IPv6 CIDR network range (e.g., 2001:db8::/64) used to verify that
+all router-IDs in the fabric are contained within this prefix.
+
+WARNING: For IPv6 fabrics to work, global forwarding needs to be enabled on all
+nodes. Check xref:pvesdn_config_fabrics_ipv6[Notes on IPv6] for how to do it and additional info.
+
+Hello Interval:: Controls how frequently (in seconds) hello packets are sent to
+discover and maintain connections with neighboring nodes. Lower values detect
+failures faster but increase network traffic. This option is global on the
+fabric, meaning every interface on every node in this fabric will inherit this
+hello-interval property. The default value is 3 seconds.
+
+CSNP Interval:: Sets how frequently (in seconds) the node synchronizes its
+routing database with neighbors. Lower values keep the network topology information
+more quickly in sync but increase network traffic. This option is global on the
+fabric, meaning every interface on every node in this fabric will inherit this
+property. The default value is 10 seconds.
+
+[[pvesdn_isis_node]]
+On the Node
+^^^^^^^^^^^
+
+[thumbnail="screenshot/gui-datacenter-create-node-isis.png"]
+
+Options that are available on every node that is part of a fabric:
+
+Node:: Select the node which will be added to the fabric. Only nodes that
+currently are in the cluster will be shown.
+
+IPv4:: A unique IPv4 address used to generate the IS-IS
+Network Entity Title (NET). Each node in the same fabric must have a different
+Router-ID, while a single node must use the same NET address across all fabrics
+(If this is not given {pve} will automatically choose one and ensure that the
+configuration is valid).
+
+IPv6:: A unique IPv6 address used to generate the IS-IS
+Network Entity Title (NET). Each node in the same fabric must have a different
+Router-ID, while a single node must use the same NET address across all fabrics.
+If a IPv4 and IPv6 address is configured, the IPv4 one will be used to derive
+the NET.
+
+WARNING: When using IPv6 addresses, the last 3 segments are used to generate
+the NET. Ensure these segments differ between nodes.
+
+Interfaces:: Specify the interfaces used to establish peering connections with
+other IS-IS nodes. Preferably select interfaces without pre-assigned IP
+addresses, then configure addresses in the IPv4/IPv6 column if needed. A dummy
+"loopback" interface with the router-id is automatically created.
+
+On The Interface
+^^^^^^^^^^^^^^^^
+
+The following optional parameters can be configured per interface when enabling
+the additional columns:
+
+IP::: A IPv4 that should get automatically configured on this interface. Must
+include the netmask (e.g. /31). If no address is set, the interface is
+configured as a `point-to-point` interface.
+
+IPv6::: A IPv6 that should get automatically configured on this interface. Must
+include the netmask (e.g. /127). If no address is set, the interface is
+configured as a `point-to-point` interface.
+
+Hello Multiplier::: Defines how many missed hello packets constitute a failed
+connection. Higher values make the connection more resilient to packet loss but
+slow down failure detection. The default value is 10.
+
+WARNING: When you remove an interface with an entry in `/etc/network/interfaces`
+that has `manual` set, then the IP will not get removed on applying the SDN
+configuration.
+
 [[pvesdn_ospf]]
 OSPF
 ~~~~
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH pve-gui-tests v3 13/13] fabrics: add screenshots for IS-IS fabric and nodes
  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
                   ` (11 preceding siblings ...)
  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 ` Gabriel Goller
  12 siblings, 0 replies; 14+ messages in thread
From: Gabriel Goller @ 2026-08-28 11:32 UTC (permalink / raw)
  To: pve-devel

Add IS-IS fabric and node screenshots.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pve/fabrics-screenshots | 47 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 47 insertions(+)

diff --git a/pve/fabrics-screenshots b/pve/fabrics-screenshots
index 726a8d88a731..b028b339ef19 100755
--- a/pve/fabrics-screenshots
+++ b/pve/fabrics-screenshots
@@ -13,6 +13,7 @@ sub prepare_fabrics {
     my ($conn) = @_;
     eval {
         # might not exist, so ignore any errors
+        $conn->delete("/cluster/sdn/fabrics/fabric/test3");
         $conn->delete("/cluster/sdn/fabrics/fabric/test2");
         $conn->delete("/cluster/sdn/fabrics/fabric/test1");
     };
@@ -29,14 +30,25 @@ sub prepare_fabrics {
         ip_prefix => "198.51.100.0/24",
         protocol => "ospf",
     };
+    my $isis = {
+        id => "test3",
+        ip_prefix => "192.0.15.0/24",
+        hello_interval => "1",
+        protocol => "isis",
+    };
     $conn->post("/cluster/sdn/fabrics/fabric", $openfabric);
     $conn->post("/cluster/sdn/fabrics/fabric", $ospf);
+    $conn->post("/cluster/sdn/fabrics/fabric", $isis);
+
 }
 
 sub prepare_nodes {
     my ($conn) = @_;
     eval {
         # might not exist, so ignore any errors
+        $conn->delete("/cluster/sdn/fabrics/node/test3/pve0");
+        $conn->delete("/cluster/sdn/fabrics/node/test3/pve1");
+        $conn->delete("/cluster/sdn/fabrics/node/test3/pve2");
         $conn->delete("/cluster/sdn/fabrics/node/test2/pve0");
         $conn->delete("/cluster/sdn/fabrics/node/test2/pve1");
         $conn->delete("/cluster/sdn/fabrics/node/test2/pve2");
@@ -52,6 +64,8 @@ sub prepare_nodes {
         my $last_octet = $i + 1;
         my $ospf_iface = ($i * 2) + 19;
         my $openfabric_iface = ($i * 2) + 25;
+        my $isis_iface = ($i * 2) + 30;
+
         
         my $ospf_node = {
             node_id => $node_name,
@@ -71,9 +85,21 @@ sub prepare_nodes {
             ],
             protocol => "openfabric",
         };
+        my $isis_node = {
+            node_id => $node_name,
+            ip => "192.0.15.$last_octet",
+            interfaces => [
+                "name=ens$isis_iface",
+                "name=ens" . ($isis_iface + 1)
+            ],
+            protocol => "isis",
+        };
+
         
         $conn->post("/cluster/sdn/fabrics/node/test1/", $openfabric_node);
         $conn->post("/cluster/sdn/fabrics/node/test2/", $ospf_node);
+        $conn->post("/cluster/sdn/fabrics/node/test3/", $isis_node);
+
     }
 }
 
@@ -141,6 +167,18 @@ sub create_fabrics_ui_screenshots {
     $gui->element_screenshot("gui-datacenter-create-fabric-openfabric.png", $window);
     $gui->window_close($window);
 
+    $menu = $gui->find_button('Add Fabric', $panel)->click();
+    $gui->find_menu_item('IS-IS')->click();
+
+    $window = $gui->find_dialog("Create: IS-IS");
+    $gui->setValue($window, 'id', 'test3');
+    $gui->setValue($window, 'ip_prefix', '192.0.2.0/24');
+    $gui->setValue($window, 'hello_interval', '1');
+
+    $gui->element_screenshot("gui-datacenter-create-fabric-isis.png", $window);
+    $gui->window_close($window);
+
+
     $menu = $gui->find_button('Add Fabric', $panel)->click();
     $gui->find_menu_item('OSPF')->click();
 
@@ -174,6 +212,15 @@ sub create_fabrics_ui_screenshots {
     $gui->element_screenshot("gui-datacenter-create-node-ospf.png", $window);
     $gui->window_close($window);
 
+    open_add_node_panel($gui, "isis");
+    sleep_ms(500);
+
+    $window = $gui->find_dialog("Create: Node");
+    $gui->setValue($window, 'ip', '198.51.100.1');
+    select_interfaces($gui, "ens19", "ens20");
+    $gui->element_screenshot("gui-datacenter-create-node-isis.png", $window);
+    $gui->window_close($window);
+
     # get fabric overview
     prepare_nodes($conn);
     sleep_ms(250);
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 14+ messages in thread

end of thread, other threads:[~2026-08-28 11:35 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH proxmox-ve-rs v3 02/13] ve-config: add IS-IS fabric config parsing and frr config generation Gabriel Goller
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

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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal