* [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking
@ 2025-10-30 15:48 Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-common 1/2] iproute2: add helper for detecting bridge members Stefan Hanreich
` (34 more replies)
0 siblings, 35 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
This patch series builds upon and replaces the two patch series initially
submitted by Gabriel [1] [2]. Main reason for merging those is that some
additional refactoring to the status API module structure as well as the UI
widgets for the SDN browser has been done, which both series would need to
depend on. Additionally, the EVPN series depended on the fabric series already
as well, so submitting them as one seemed like the least complicated option for
both developers and maintainers with the additional changes introduced in this
iteration.
## Rationale
Currently, the SDN and PVE networking stack provide little insight into their
inner workings and can be a bit of a black box to users. Inspecting the current
state of networking resources, particularly for EVPN zones, requires dropping
into the CLI and invoking specific iproute2 / FRR commands. The current status
endpoint only provides very coarse and limited feedback on the current state of
SDN / networking resources.
With this iteration, this patch series also adds status reporting for bridges /
vnets, which has been requested several times in forums / enterprise support /
trainings.
Most of those endpoints could be interesting additions to the PDM UI as well,
particularly fabrics and evpn status.
## New network resource type
While the initial implementations extended the existing SDN resource type, this
iteration introduces a 'network' resource type. The pre-existing SDN resource
type utilized 'sdn/<zone_id>' as its id, which makes it hard to add additional
types that do not share that ID space. Changing the schema for the ID would also
break backwards-compatibility of API and UI between 9.0 and 9.1.
With potential additional status reporting for other network entities (see
below), it would make sense to generalize the resource type to network in
particular, to avoid cluttering the top level with one type per SDN/networking
entity. If that is not a concern, the current state could be easily adapted to
have one top-level type per resource - simplifying the current implementation.
The ID schema for this resource type is now as follow:
network/{node}/{network_type}/{name}
An example network resource:
{
"id": "network/acolyte/fabric/underlay",
"type": "network",
"network_type": "fabric",
"network": "underlay"
"node": "acolyte",
"status": "ok",
"protocol": "ospf",
}
The plan for migrating:
* The zones will still report their status as they did before (SDN resource
type)
* New networking entities (fabrics, for now) will utilize the network resource
type
* We will add support for parsing the new zone status reporting format in this
patch series (but it will not be sent by any node)
* When migrating from PVE 9 -> 10, status reporting for zones will move to the
new network resource type
* old nodes should be able to cope with the new format, since support for it has
been included for awhile
I know this is a bit of a sledgehammer method of solving this problem, but imo
while this migration might be a bit painful now, it seems the best option to me
long-term. Any suggestions / opinions on this would be greatly appreciated. I
don't really see another way of implementing additional types of entities
without either breaking backwards-compatibility with PVE <= 9.0 or having
potential ID collisions in the SDN resource type or having one dedicated type
per networking resource.
## Potential future work / extensions
Add status reporting for the firewall, which currently acts a bit like a
black-box as well, without any easy way of checking the current (running) state
of the firewall.
Other entities to consider adding to the resources: controllers, DNS, external
IPAM.
The data from those endpoints could be used to provide a graphical overview of a
bridge in the UI, an idea which has been floating around internally for awhile.
## New API endpoints
/nodes/{node}/sdn/fabrics/{fabric}/routes
/nodes/{node}/sdn/fabrics/{fabric}/neighbors
/nodes/{node}/sdn/fabrics/{fabric}/interfaces
/nodes/{node}/sdn/zones/{zone}/ip-vrf
/nodes/{node}/sdn/zones/{zone}/bridges
/nodes/{node}/sdn/vnets/{vnet}/mac-vrf
## New UI panels
Those panels can all be reached via the resource tree and are found in the SDN
browser.
For all zones:
* Bridges overview
For EVPN zones:
* IP-VRF
* MAC-VRFs
For Fabrics:
* Routes
* Neighbors
* Interfaces
## Dependencies
proxmox-perl-rs depends on proxmox-ve-rs
pve-network depends on proxmox-perl-rs
pve-network depends on pve-common
pve-manager depends on pve-network
Changes from (v1, v4):
* refactor the SDN status API module structure (no functional changes to
existing endpoints)
* move the fabrics API endpoints to the pre-existing /nodes/{node}/sdn subdir
* refactor the SDN content view panel, so it can be reused for the EVPN panels
(no functional changes to existing UI panels)
* add a completely new resource type, instead of trying to re-use the existing
SDN one (reasoning above).
* move the iproute2 and bridge helpers to pve-common
* improve JSONSchema of all API endpoints (descriptions mainly)
* return additional information in the fabric endpoints
* add full UI integration for EVPN status (IP-VRF + MAC-VRF panels)
* Use the installed, duplicate and bestpath properties of FRR to show only
routes that are actually installed into the kernel routing table for EVPN
zones
* filter for type 2 routes specifically when invoking vtysh
[1] https://lore.proxmox.com/pve-devel/20250904114206.193052-1-g.goller@proxmox.com/
[2] https://lore.proxmox.com/pve-devel/20250905114504.195110-1-g.goller@proxmox.com/
pve-common:
Stefan Hanreich (2):
iproute2: add helper for detecting bridge members
iproute2: add helper for querying vlan information
src/PVE/IPRoute2.pm | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
proxmox-ve-rs:
Gabriel Goller (6):
frr: make room for deserialization structs
frr: add deserialization types for openfabric and ospf
ve-config: add helper function to iterate over all nodes in all
fabrics
ve-config: add optional tag property to vnet
frr: fix some route deserialization types
frr: add deserialization types for EVPN
proxmox-frr/Cargo.toml | 2 +
proxmox-frr/debian/control | 6 +
proxmox-frr/src/de/evpn.rs | 165 ++++++++++++
proxmox-frr/src/de/mod.rs | 49 ++++
proxmox-frr/src/de/openfabric.rs | 101 ++++++++
proxmox-frr/src/de/ospf.rs | 64 +++++
proxmox-frr/src/lib.rs | 243 +-----------------
proxmox-frr/src/ser/mod.rs | 241 +++++++++++++++++
proxmox-frr/src/{ => ser}/openfabric.rs | 4 +-
proxmox-frr/src/{ => ser}/ospf.rs | 2 +-
proxmox-frr/src/{ => ser}/route_map.rs | 0
proxmox-frr/src/{ => ser}/serializer.rs | 2 +-
proxmox-ve-config/src/sdn/config.rs | 27 +-
proxmox-ve-config/src/sdn/fabric/frr.rs | 170 ++++++------
proxmox-ve-config/src/sdn/fabric/mod.rs | 5 +
proxmox-ve-config/src/sdn/frr.rs | 2 +-
proxmox-ve-config/tests/fabric/main.rs | 2 +-
proxmox-ve-config/tests/sdn/main.rs | 5 +-
.../tests/sdn/resources/running-config.json | 1 +
19 files changed, 761 insertions(+), 330 deletions(-)
create mode 100644 proxmox-frr/src/de/evpn.rs
create mode 100644 proxmox-frr/src/de/mod.rs
create mode 100644 proxmox-frr/src/de/openfabric.rs
create mode 100644 proxmox-frr/src/de/ospf.rs
create mode 100644 proxmox-frr/src/ser/mod.rs
rename proxmox-frr/src/{ => ser}/openfabric.rs (97%)
rename proxmox-frr/src/{ => ser}/ospf.rs (99%)
rename proxmox-frr/src/{ => ser}/route_map.rs (100%)
rename proxmox-frr/src/{ => ser}/serializer.rs (99%)
proxmox-perl-rs:
Gabriel Goller (9):
pve-rs: firewall: cargo: fmt
pve-rs: cargo: bump proxmox-apt and proxmox-ve-config versions
pve-rs: fabrics: update proxmox-frr import path
pve-rs: fabrics: fix clippy lint warnings
pve-rs: fabrics: add function to get status of fabric
pve-rs: fabrics: add function to get l2vpn and l3vpn routes for evpn
pve-rs: fabrics: add function to get routes learned by a fabric
pve-rs: fabrics: add function to get the interfaces used for a fabric
pve-rs: fabrics: add function to get the neighbors for a fabric
Stefan Hanreich (1):
pve-rs: firewall: add missing documentation comments
pve-rs/Cargo.toml | 4 +-
pve-rs/src/bindings/firewall/sdn.rs | 16 +-
pve-rs/src/bindings/sdn/fabrics.rs | 296 +++++++++++++-
pve-rs/src/lib.rs | 2 +
pve-rs/src/sdn/mod.rs | 3 +
pve-rs/src/sdn/status.rs | 585 ++++++++++++++++++++++++++++
6 files changed, 896 insertions(+), 10 deletions(-)
create mode 100644 pve-rs/src/sdn/mod.rs
create mode 100644 pve-rs/src/sdn/status.rs
pve-network:
Gabriel Goller (3):
fabrics: add fabrics status to SDN::status function
api: nodes: fabrics: add endpoint for querying route status
api: nodes: fabrics: add endpoint for querying neighbor information
Stefan Hanreich (6):
refactor: rework api module structure for the /nodes/{node}/sdn subdir
sdn: status: add zone type to sdn resource
api: nodes: fabrics: add endpoint for querying interface status
api: nodes: zones: add bridge status
api: nodes: zones: add ip vrf endpoint for evpn zones
api: nodes: vnets: add mac-vrf endpoint for evpn vnets
src/PVE/API2/Network/SDN/Makefile | 2 +-
src/PVE/API2/Network/SDN/Nodes/Fabric.pm | 187 +++++++++
src/PVE/API2/Network/SDN/Nodes/Fabrics.pm | 16 +
.../Network/SDN/{Zones => Nodes}/Makefile | 12 +-
src/PVE/API2/Network/SDN/Nodes/Status.pm | 61 +++
src/PVE/API2/Network/SDN/Nodes/Vnet.pm | 147 +++++++
src/PVE/API2/Network/SDN/Nodes/Vnets.pm | 16 +
src/PVE/API2/Network/SDN/Nodes/Zone.pm | 379 ++++++++++++++++++
.../SDN/{Zones/Status.pm => Nodes/Zones.pm} | 58 +--
src/PVE/API2/Network/SDN/Vnets.pm | 2 +-
src/PVE/API2/Network/SDN/Zones/Content.pm | 88 ----
src/PVE/Network/SDN.pm | 6 +-
src/PVE/Network/SDN/Zones.pm | 2 +
src/test/debug/statuscheck.pl | 3 +-
14 files changed, 833 insertions(+), 146 deletions(-)
create mode 100644 src/PVE/API2/Network/SDN/Nodes/Fabric.pm
create mode 100644 src/PVE/API2/Network/SDN/Nodes/Fabrics.pm
rename src/PVE/API2/Network/SDN/{Zones => Nodes}/Makefile (51%)
create mode 100644 src/PVE/API2/Network/SDN/Nodes/Status.pm
create mode 100644 src/PVE/API2/Network/SDN/Nodes/Vnet.pm
create mode 100644 src/PVE/API2/Network/SDN/Nodes/Vnets.pm
create mode 100644 src/PVE/API2/Network/SDN/Nodes/Zone.pm
rename src/PVE/API2/Network/SDN/{Zones/Status.pm => Nodes/Zones.pm} (56%)
delete mode 100644 src/PVE/API2/Network/SDN/Zones/Content.pm
pve-manager:
Gabriel Goller (2):
pvestatd: add network resource to status reporting
ui: resource tree: add network resource
Stefan Hanreich (6):
api: nodes: use new status module for sdn subdirectory
refactor: ui: sdn browser: parametrize zone content panel
pvestatd: sdn: adapt to changes in status reporting
ui: sdn browser: Add ip-vrf panel for evpn zones
ui: sdn browser: add mac vrf panel
ui: sdn browser: add zone bridge view
PVE/API2/Cluster.pm | 102 ++++++++++++---
PVE/API2/Nodes.pm | 50 +------
PVE/Service/pvestatd.pm | 29 ++--
www/manager6/Makefile | 6 +
www/manager6/Utils.js | 11 ++
www/manager6/Workspace.js | 1 +
www/manager6/sdn/Browser.js | 29 ++++
www/manager6/sdn/EvpnZoneIpVrfPanel.js | 84 ++++++++++++
www/manager6/sdn/EvpnZoneMacVrfPanel.js | 130 ++++++++++++++++++
www/manager6/sdn/FabricsContentView.js | 77 +++++++++++
www/manager6/sdn/NetworkBrowser.js | 167 ++++++++++++++++++++++++
www/manager6/sdn/ZoneBridgeView.js | 88 +++++++++++++
www/manager6/sdn/ZoneBridgesPanel.js | 131 +++++++++++++++++++
www/manager6/sdn/ZoneContentPanel.js | 11 +-
www/manager6/sdn/ZoneContentView.js | 75 ++++++-----
www/manager6/tree/ResourceTree.js | 6 +
16 files changed, 888 insertions(+), 109 deletions(-)
create mode 100644 www/manager6/sdn/EvpnZoneIpVrfPanel.js
create mode 100644 www/manager6/sdn/EvpnZoneMacVrfPanel.js
create mode 100644 www/manager6/sdn/FabricsContentView.js
create mode 100644 www/manager6/sdn/NetworkBrowser.js
create mode 100644 www/manager6/sdn/ZoneBridgeView.js
create mode 100644 www/manager6/sdn/ZoneBridgesPanel.js
Summary over all repositories:
56 files changed, 3401 insertions(+), 595 deletions(-)
--
Generated by git-murpp 0.8.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-common 1/2] iproute2: add helper for detecting bridge members
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-common 2/2] iproute2: add helper for querying vlan information Stefan Hanreich
` (33 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
Used in the SDN zone bridges status endpoint for querying the members
of a specific bridge.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/IPRoute2.pm | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/PVE/IPRoute2.pm b/src/PVE/IPRoute2.pm
index 70007f5..4e31376 100644
--- a/src/PVE/IPRoute2.pm
+++ b/src/PVE/IPRoute2.pm
@@ -32,6 +32,11 @@ sub ip_link_is_physical($ip_link) {
&& (!defined($ip_link->{linkinfo}) || !defined($ip_link->{linkinfo}->{info_kind}));
}
+sub ip_link_is_bridge_member($ip_link) {
+ return defined($ip_link->{linkinfo}->{info_slave_kind})
+ && $ip_link->{linkinfo}->{info_slave_kind} eq "bridge";
+}
+
sub altname_mapping($ip_links) {
$ip_links = ip_link_details() if !defined($ip_links);
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-common 2/2] iproute2: add helper for querying vlan information
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-common 1/2] iproute2: add helper for detecting bridge members Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 1/6] frr: make room for deserialization structs Stefan Hanreich
` (32 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
'-compressvlans' is used by default, because PVE configures VLAN
1-4094 by default on all ports on a VLAN-aware bridge and having one
entry per VLAN in this case is quite excessive.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/IPRoute2.pm | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/src/PVE/IPRoute2.pm b/src/PVE/IPRoute2.pm
index 4e31376..fcacabf 100644
--- a/src/PVE/IPRoute2.pm
+++ b/src/PVE/IPRoute2.pm
@@ -55,4 +55,22 @@ sub altname_mapping($ip_links) {
return $altnames;
}
+sub get_vlan_information() {
+ my $bridge_output = '';
+
+ run_command(
+ [
+ 'bridge', '-compressvlan', '-json', 'vlan', 'show',
+ ],
+ outfunc => sub {
+ $bridge_output .= shift;
+ },
+ );
+
+ my $data = decode_json($bridge_output);
+ my %vlan_information = map { $_->{ifname} => $_ } $data->@*;
+
+ return \%vlan_information;
+}
+
1;
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs 1/6] frr: make room for deserialization structs
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-common 1/2] iproute2: add helper for detecting bridge members Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-common 2/2] iproute2: add helper for querying vlan information Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 2/6] frr: add deserialization types for openfabric and ospf Stefan Hanreich
` (31 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Move all the serialization structs to a new subfolder `ser`. This makes
place for the deserialization structs which will land in a new folder
`de`. The deserialization structs will be used to parse the output of
`vtysh` commands, so that we can show statistics of various sdn objects.
Also update all the callsites to use the new subfolder.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-frr/src/lib.rs | 242 +-----------------------
proxmox-frr/src/ser/mod.rs | 241 +++++++++++++++++++++++
proxmox-frr/src/{ => ser}/openfabric.rs | 4 +-
proxmox-frr/src/{ => ser}/ospf.rs | 2 +-
proxmox-frr/src/{ => ser}/route_map.rs | 0
proxmox-frr/src/{ => ser}/serializer.rs | 2 +-
proxmox-ve-config/src/sdn/fabric/frr.rs | 170 +++++++++--------
proxmox-ve-config/src/sdn/frr.rs | 2 +-
proxmox-ve-config/tests/fabric/main.rs | 2 +-
9 files changed, 340 insertions(+), 325 deletions(-)
create mode 100644 proxmox-frr/src/ser/mod.rs
rename proxmox-frr/src/{ => ser}/openfabric.rs (97%)
rename proxmox-frr/src/{ => ser}/ospf.rs (99%)
rename proxmox-frr/src/{ => ser}/route_map.rs (100%)
rename proxmox-frr/src/{ => ser}/serializer.rs (99%)
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index 8610118..35b62cb 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -1,241 +1 @@
-pub mod openfabric;
-pub mod ospf;
-pub mod route_map;
-pub mod serializer;
-
-use std::collections::{BTreeMap, BTreeSet};
-use std::fmt::Display;
-use std::str::FromStr;
-
-use crate::route_map::{AccessList, ProtocolRouteMap, RouteMap};
-
-use thiserror::Error;
-
-/// Generic FRR router.
-///
-/// This generic FRR router contains all the protocols that we implement.
-/// In FRR this is e.g.:
-/// ```text
-/// router openfabric test
-/// !....
-/// ! or
-/// router ospf
-/// !....
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum Router {
- Openfabric(openfabric::OpenfabricRouter),
- Ospf(ospf::OspfRouter),
-}
-
-impl From<openfabric::OpenfabricRouter> for Router {
- fn from(value: openfabric::OpenfabricRouter) -> Self {
- Router::Openfabric(value)
- }
-}
-
-/// Generic FRR routername.
-///
-/// The variants represent different protocols. Some have `router <protocol> <name>`, others have
-/// `router <protocol> <process-id>`, some only have `router <protocol>`.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum RouterName {
- Openfabric(openfabric::OpenfabricRouterName),
- Ospf(ospf::OspfRouterName),
-}
-
-impl From<openfabric::OpenfabricRouterName> for RouterName {
- fn from(value: openfabric::OpenfabricRouterName) -> Self {
- Self::Openfabric(value)
- }
-}
-
-impl Display for RouterName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Openfabric(r) => r.fmt(f),
- Self::Ospf(r) => r.fmt(f),
- }
- }
-}
-
-/// The interface name is the same on ospf and openfabric, but it is an enum so that we can have
-/// two different entries in the btreemap. This allows us to have an interface in a ospf and
-/// openfabric fabric.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub enum InterfaceName {
- Openfabric(CommonInterfaceName),
- Ospf(CommonInterfaceName),
-}
-
-impl Display for InterfaceName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- InterfaceName::Openfabric(frr_word) => frr_word.fmt(f),
- InterfaceName::Ospf(frr_word) => frr_word.fmt(f),
- }
- }
-}
-
-/// Generic FRR Interface.
-///
-/// In FRR config it looks like this:
-/// ```text
-/// interface <name>
-/// ! ...
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
-pub enum Interface {
- Openfabric(openfabric::OpenfabricInterface),
- Ospf(ospf::OspfInterface),
-}
-
-impl From<openfabric::OpenfabricInterface> for Interface {
- fn from(value: openfabric::OpenfabricInterface) -> Self {
- Self::Openfabric(value)
- }
-}
-
-impl From<ospf::OspfInterface> for Interface {
- fn from(value: ospf::OspfInterface) -> Self {
- Self::Ospf(value)
- }
-}
-
-#[derive(Error, Debug)]
-pub enum FrrWordError {
- #[error("word is empty")]
- IsEmpty,
- #[error("word contains invalid character")]
- InvalidCharacter,
-}
-
-/// A simple FRR Word.
-///
-/// Every string argument or value in FRR is an FrrWord. FrrWords must only contain ascii
-/// characters and must not have a whitespace.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct FrrWord(String);
-
-impl FrrWord {
- pub fn new<T: AsRef<str> + Into<String>>(name: T) -> Result<Self, FrrWordError> {
- if name.as_ref().is_empty() {
- return Err(FrrWordError::IsEmpty);
- }
-
- if name
- .as_ref()
- .as_bytes()
- .iter()
- .any(|c| !c.is_ascii() || c.is_ascii_whitespace())
- {
- eprintln!("invalid char in: \"{}\"", name.as_ref());
- return Err(FrrWordError::InvalidCharacter);
- }
-
- Ok(Self(name.into()))
- }
-}
-
-impl FromStr for FrrWord {
- type Err = FrrWordError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- Self::new(s)
- }
-}
-
-impl Display for FrrWord {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
- }
-}
-
-impl AsRef<str> for FrrWord {
- fn as_ref(&self) -> &str {
- &self.0
- }
-}
-
-#[derive(Error, Debug)]
-pub enum CommonInterfaceNameError {
- #[error("interface name too long")]
- TooLong,
-}
-
-/// Name of a interface, which is common between all protocols.
-///
-/// FRR itself doesn't enforce any limits, but the kernel does. Linux only allows interface names
-/// to be a maximum of 16 bytes. This is enforced by this struct.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct CommonInterfaceName(String);
-
-impl TryFrom<&str> for CommonInterfaceName {
- type Error = CommonInterfaceNameError;
-
- fn try_from(value: &str) -> Result<Self, Self::Error> {
- Self::new(value)
- }
-}
-
-impl TryFrom<String> for CommonInterfaceName {
- type Error = CommonInterfaceNameError;
-
- fn try_from(value: String) -> Result<Self, Self::Error> {
- Self::new(value)
- }
-}
-
-impl CommonInterfaceName {
- pub fn new<T: AsRef<str> + Into<String>>(s: T) -> Result<Self, CommonInterfaceNameError> {
- if s.as_ref().len() <= 15 {
- Ok(Self(s.into()))
- } else {
- Err(CommonInterfaceNameError::TooLong)
- }
- }
-}
-
-impl Display for CommonInterfaceName {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
- }
-}
-
-/// Main FRR config.
-///
-/// Contains the two main frr building blocks: routers and interfaces. It also holds other
-/// top-level FRR options, such as access-lists, router-maps and protocol-routemaps. This struct
-/// gets generated using the `FrrConfigBuilder` in `proxmox-ve-config`.
-#[derive(Clone, Debug, PartialEq, Eq, Default)]
-pub struct FrrConfig {
- pub router: BTreeMap<RouterName, Router>,
- pub interfaces: BTreeMap<InterfaceName, Interface>,
- pub access_lists: Vec<AccessList>,
- pub routemaps: Vec<RouteMap>,
- pub protocol_routemaps: BTreeSet<ProtocolRouteMap>,
-}
-
-impl FrrConfig {
- pub fn new() -> Self {
- Self::default()
- }
-
- pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
- self.router.iter()
- }
-
- pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
- self.interfaces.iter()
- }
-
- pub fn access_lists(&self) -> impl Iterator<Item = &AccessList> + '_ {
- self.access_lists.iter()
- }
- pub fn routemaps(&self) -> impl Iterator<Item = &RouteMap> + '_ {
- self.routemaps.iter()
- }
-
- pub fn protocol_routemaps(&self) -> impl Iterator<Item = &ProtocolRouteMap> + '_ {
- self.protocol_routemaps.iter()
- }
-}
+pub mod ser;
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
new file mode 100644
index 0000000..a90397b
--- /dev/null
+++ b/proxmox-frr/src/ser/mod.rs
@@ -0,0 +1,241 @@
+pub mod openfabric;
+pub mod ospf;
+pub mod route_map;
+pub mod serializer;
+
+use std::collections::{BTreeMap, BTreeSet};
+use std::fmt::Display;
+use std::str::FromStr;
+
+use crate::ser::route_map::{AccessList, ProtocolRouteMap, RouteMap};
+
+use thiserror::Error;
+
+/// Generic FRR router.
+///
+/// This generic FRR router contains all the protocols that we implement.
+/// In FRR this is e.g.:
+/// ```text
+/// router openfabric test
+/// !....
+/// ! or
+/// router ospf
+/// !....
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum Router {
+ Openfabric(openfabric::OpenfabricRouter),
+ Ospf(ospf::OspfRouter),
+}
+
+impl From<openfabric::OpenfabricRouter> for Router {
+ fn from(value: openfabric::OpenfabricRouter) -> Self {
+ Router::Openfabric(value)
+ }
+}
+
+/// Generic FRR routername.
+///
+/// The variants represent different protocols. Some have `router <protocol> <name>`, others have
+/// `router <protocol> <process-id>`, some only have `router <protocol>`.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum RouterName {
+ Openfabric(openfabric::OpenfabricRouterName),
+ Ospf(ospf::OspfRouterName),
+}
+
+impl From<openfabric::OpenfabricRouterName> for RouterName {
+ fn from(value: openfabric::OpenfabricRouterName) -> Self {
+ Self::Openfabric(value)
+ }
+}
+
+impl Display for RouterName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Openfabric(r) => r.fmt(f),
+ Self::Ospf(r) => r.fmt(f),
+ }
+ }
+}
+
+/// The interface name is the same on ospf and openfabric, but it is an enum so that we can have
+/// two different entries in the btreemap. This allows us to have an interface in a ospf and
+/// openfabric fabric.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum InterfaceName {
+ Openfabric(CommonInterfaceName),
+ Ospf(CommonInterfaceName),
+}
+
+impl Display for InterfaceName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ InterfaceName::Openfabric(frr_word) => frr_word.fmt(f),
+ InterfaceName::Ospf(frr_word) => frr_word.fmt(f),
+ }
+ }
+}
+
+/// Generic FRR Interface.
+///
+/// In FRR config it looks like this:
+/// ```text
+/// interface <name>
+/// ! ...
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum Interface {
+ Openfabric(openfabric::OpenfabricInterface),
+ Ospf(ospf::OspfInterface),
+}
+
+impl From<openfabric::OpenfabricInterface> for Interface {
+ fn from(value: openfabric::OpenfabricInterface) -> Self {
+ Self::Openfabric(value)
+ }
+}
+
+impl From<ospf::OspfInterface> for Interface {
+ fn from(value: ospf::OspfInterface) -> Self {
+ Self::Ospf(value)
+ }
+}
+
+#[derive(Error, Debug)]
+pub enum FrrWordError {
+ #[error("word is empty")]
+ IsEmpty,
+ #[error("word contains invalid character")]
+ InvalidCharacter,
+}
+
+/// A simple FRR Word.
+///
+/// Every string argument or value in FRR is an FrrWord. FrrWords must only contain ascii
+/// characters and must not have a whitespace.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct FrrWord(String);
+
+impl FrrWord {
+ pub fn new<T: AsRef<str> + Into<String>>(name: T) -> Result<Self, FrrWordError> {
+ if name.as_ref().is_empty() {
+ return Err(FrrWordError::IsEmpty);
+ }
+
+ if name
+ .as_ref()
+ .as_bytes()
+ .iter()
+ .any(|c| !c.is_ascii() || c.is_ascii_whitespace())
+ {
+ eprintln!("invalid char in: \"{}\"", name.as_ref());
+ return Err(FrrWordError::InvalidCharacter);
+ }
+
+ Ok(Self(name.into()))
+ }
+}
+
+impl FromStr for FrrWord {
+ type Err = FrrWordError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Self::new(s)
+ }
+}
+
+impl Display for FrrWord {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl AsRef<str> for FrrWord {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+}
+
+#[derive(Error, Debug)]
+pub enum CommonInterfaceNameError {
+ #[error("interface name too long")]
+ TooLong,
+}
+
+/// Name of a interface, which is common between all protocols.
+///
+/// FRR itself doesn't enforce any limits, but the kernel does. Linux only allows interface names
+/// to be a maximum of 16 bytes. This is enforced by this struct.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct CommonInterfaceName(String);
+
+impl TryFrom<&str> for CommonInterfaceName {
+ type Error = CommonInterfaceNameError;
+
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
+ Self::new(value)
+ }
+}
+
+impl TryFrom<String> for CommonInterfaceName {
+ type Error = CommonInterfaceNameError;
+
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ Self::new(value)
+ }
+}
+
+impl CommonInterfaceName {
+ pub fn new<T: AsRef<str> + Into<String>>(s: T) -> Result<Self, CommonInterfaceNameError> {
+ if s.as_ref().len() <= 15 {
+ Ok(Self(s.into()))
+ } else {
+ Err(CommonInterfaceNameError::TooLong)
+ }
+ }
+}
+
+impl Display for CommonInterfaceName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+/// Main FRR config.
+///
+/// Contains the two main frr building blocks: routers and interfaces. It also holds other
+/// top-level FRR options, such as access-lists, router-maps and protocol-routemaps. This struct
+/// gets generated using the `FrrConfigBuilder` in `proxmox-ve-config`.
+#[derive(Clone, Debug, PartialEq, Eq, Default)]
+pub struct FrrConfig {
+ pub router: BTreeMap<RouterName, Router>,
+ pub interfaces: BTreeMap<InterfaceName, Interface>,
+ pub access_lists: Vec<AccessList>,
+ pub routemaps: Vec<RouteMap>,
+ pub protocol_routemaps: BTreeSet<ProtocolRouteMap>,
+}
+
+impl FrrConfig {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
+ self.router.iter()
+ }
+
+ pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
+ self.interfaces.iter()
+ }
+
+ pub fn access_lists(&self) -> impl Iterator<Item = &AccessList> + '_ {
+ self.access_lists.iter()
+ }
+ pub fn routemaps(&self) -> impl Iterator<Item = &RouteMap> + '_ {
+ self.routemaps.iter()
+ }
+
+ pub fn protocol_routemaps(&self) -> impl Iterator<Item = &ProtocolRouteMap> + '_ {
+ self.protocol_routemaps.iter()
+ }
+}
diff --git a/proxmox-frr/src/openfabric.rs b/proxmox-frr/src/ser/openfabric.rs
similarity index 97%
rename from proxmox-frr/src/openfabric.rs
rename to proxmox-frr/src/ser/openfabric.rs
index 6e2a720..0f0c650 100644
--- a/proxmox-frr/src/openfabric.rs
+++ b/proxmox-frr/src/ser/openfabric.rs
@@ -5,8 +5,8 @@ use proxmox_sdn_types::net::Net;
use thiserror::Error;
-use crate::FrrWord;
-use crate::FrrWordError;
+use crate::ser::FrrWord;
+use crate::ser::FrrWordError;
/// The name of a OpenFabric router. Is an FrrWord.
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
diff --git a/proxmox-frr/src/ospf.rs b/proxmox-frr/src/ser/ospf.rs
similarity index 99%
rename from proxmox-frr/src/ospf.rs
rename to proxmox-frr/src/ser/ospf.rs
index d0e098e..67e39a4 100644
--- a/proxmox-frr/src/ospf.rs
+++ b/proxmox-frr/src/ser/ospf.rs
@@ -4,7 +4,7 @@ use std::net::Ipv4Addr;
use thiserror::Error;
-use crate::{FrrWord, FrrWordError};
+use crate::ser::{FrrWord, FrrWordError};
/// The name of the ospf frr router.
///
diff --git a/proxmox-frr/src/route_map.rs b/proxmox-frr/src/ser/route_map.rs
similarity index 100%
rename from proxmox-frr/src/route_map.rs
rename to proxmox-frr/src/ser/route_map.rs
diff --git a/proxmox-frr/src/serializer.rs b/proxmox-frr/src/ser/serializer.rs
similarity index 99%
rename from proxmox-frr/src/serializer.rs
rename to proxmox-frr/src/ser/serializer.rs
index f8a3c72..3a681e2 100644
--- a/proxmox-frr/src/serializer.rs
+++ b/proxmox-frr/src/ser/serializer.rs
@@ -1,6 +1,6 @@
use std::fmt::{self, Write};
-use crate::{
+use crate::ser::{
openfabric::{OpenfabricInterface, OpenfabricRouter},
ospf::{OspfInterface, OspfRouter},
route_map::{AccessList, AccessListName, ProtocolRouteMap, RouteMap},
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index 486f7dc..10025b3 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -1,12 +1,7 @@
use std::net::{IpAddr, Ipv4Addr};
use tracing;
-use proxmox_frr::ospf::{self, NetworkType};
-use proxmox_frr::route_map::{
- AccessAction, AccessList, AccessListName, AccessListRule, ProtocolRouteMap, ProtocolType,
- RouteMap, RouteMapMatch, RouteMapMatchInner, RouteMapName, RouteMapSet,
-};
-use proxmox_frr::{FrrConfig, FrrWord, Interface, InterfaceName, Router, RouterName};
+use proxmox_frr::ser::{self};
use proxmox_network_types::ip_address::Cidr;
use proxmox_sdn_types::net::Net;
@@ -26,7 +21,7 @@ use crate::sdn::fabric::{FabricConfig, FabricEntry};
pub fn build_fabric(
current_node: NodeId,
config: Valid<FabricConfig>,
- frr_config: &mut FrrConfig,
+ frr_config: &mut ser::FrrConfig,
) -> Result<(), anyhow::Error> {
let mut routemap_seq = 100;
let mut current_router_id: Option<Ipv4Addr> = None;
@@ -93,27 +88,31 @@ pub fn build_fabric(
}
if let Some(ipv4cidr) = fabric.ip_prefix() {
- let rule = AccessListRule {
- action: AccessAction::Permit,
+ let rule = ser::route_map::AccessListRule {
+ action: ser::route_map::AccessAction::Permit,
network: Cidr::from(ipv4cidr),
seq: None,
};
- let access_list_name =
- AccessListName::new(format!("pve_openfabric_{}_ips", fabric_id));
- frr_config.access_lists.push(AccessList {
+ let access_list_name = ser::route_map::AccessListName::new(format!(
+ "pve_openfabric_{}_ips",
+ fabric_id
+ ));
+ frr_config.access_lists.push(ser::route_map::AccessList {
name: access_list_name,
rules: vec![rule],
});
}
if let Some(ipv6cidr) = fabric.ip6_prefix() {
- let rule = AccessListRule {
- action: AccessAction::Permit,
+ let rule = ser::route_map::AccessListRule {
+ action: ser::route_map::AccessAction::Permit,
network: Cidr::from(ipv6cidr),
seq: None,
};
- let access_list_name =
- AccessListName::new(format!("pve_openfabric_{}_ip6s", fabric_id));
- frr_config.access_lists.push(AccessList {
+ let access_list_name = ser::route_map::AccessListName::new(format!(
+ "pve_openfabric_{}_ip6s",
+ fabric_id
+ ));
+ frr_config.access_lists.push(ser::route_map::AccessList {
name: access_list_name,
rules: vec![rule],
});
@@ -128,10 +127,12 @@ pub fn build_fabric(
));
routemap_seq += 10;
- let protocol_routemap = ProtocolRouteMap {
+ let protocol_routemap = ser::route_map::ProtocolRouteMap {
is_ipv6: false,
- protocol: ProtocolType::Openfabric,
- routemap_name: RouteMapName::new("pve_openfabric".to_owned()),
+ protocol: ser::route_map::ProtocolType::Openfabric,
+ routemap_name: ser::route_map::RouteMapName::new(
+ "pve_openfabric".to_owned(),
+ ),
};
frr_config.protocol_routemaps.insert(protocol_routemap);
@@ -145,10 +146,12 @@ pub fn build_fabric(
));
routemap_seq += 10;
- let protocol_routemap = ProtocolRouteMap {
+ let protocol_routemap = ser::route_map::ProtocolRouteMap {
is_ipv6: true,
- protocol: ProtocolType::Openfabric,
- routemap_name: RouteMapName::new("pve_openfabric6".to_owned()),
+ protocol: ser::route_map::ProtocolType::Openfabric,
+ routemap_name: ser::route_map::RouteMapName::new(
+ "pve_openfabric6".to_owned(),
+ ),
};
frr_config.protocol_routemaps.insert(protocol_routemap);
@@ -164,8 +167,8 @@ pub fn build_fabric(
let fabric = ospf_entry.fabric_section();
- let frr_word_area = FrrWord::new(fabric.properties().area.to_string())?;
- let frr_area = ospf::Area::new(frr_word_area)?;
+ let frr_word_area = ser::FrrWord::new(fabric.properties().area.to_string())?;
+ let frr_area = ser::ospf::Area::new(frr_word_area)?;
let (router_name, router_item) = build_ospf_router(*router_id)?;
frr_config.router.insert(router_name, router_item);
@@ -196,17 +199,18 @@ pub fn build_fabric(
}
}
- let access_list_name = AccessListName::new(format!("pve_ospf_{}_ips", fabric_id));
+ let access_list_name =
+ ser::route_map::AccessListName::new(format!("pve_ospf_{}_ips", fabric_id));
- let rule = AccessListRule {
- action: AccessAction::Permit,
+ let rule = ser::route_map::AccessListRule {
+ action: ser::route_map::AccessAction::Permit,
network: Cidr::from(
fabric.ip_prefix().expect("fabric must have a ipv4 prefix"),
),
seq: None,
};
- frr_config.access_lists.push(AccessList {
+ frr_config.access_lists.push(ser::route_map::AccessList {
name: access_list_name,
rules: vec![rule],
});
@@ -220,10 +224,10 @@ pub fn build_fabric(
routemap_seq += 10;
frr_config.routemaps.push(routemap);
- let protocol_routemap = ProtocolRouteMap {
+ let protocol_routemap = ser::route_map::ProtocolRouteMap {
is_ipv6: false,
- protocol: ProtocolType::Ospf,
- routemap_name: RouteMapName::new("pve_ospf".to_owned()),
+ protocol: ser::route_map::ProtocolType::Ospf,
+ routemap_name: ser::route_map::RouteMapName::new("pve_ospf".to_owned()),
};
frr_config.protocol_routemaps.insert(protocol_routemap);
@@ -234,10 +238,10 @@ pub fn build_fabric(
}
/// Helper that builds a OSPF router with a the router_id.
-fn build_ospf_router(router_id: Ipv4Addr) -> Result<(RouterName, Router), anyhow::Error> {
- let ospf_router = proxmox_frr::ospf::OspfRouter { router_id };
- let router_item = Router::Ospf(ospf_router);
- let router_name = RouterName::Ospf(proxmox_frr::ospf::OspfRouterName);
+fn build_ospf_router(router_id: Ipv4Addr) -> Result<(ser::RouterName, ser::Router), anyhow::Error> {
+ let ospf_router = ser::ospf::OspfRouter { router_id };
+ let router_item = ser::Router::Ospf(ospf_router);
+ let router_name = ser::RouterName::Ospf(ser::ospf::OspfRouterName);
Ok((router_name, router_item))
}
@@ -245,45 +249,45 @@ fn build_ospf_router(router_id: Ipv4Addr) -> Result<(RouterName, Router), anyhow
fn build_openfabric_router(
fabric_id: &FabricId,
net: Net,
-) -> Result<(RouterName, Router), anyhow::Error> {
- let ofr = proxmox_frr::openfabric::OpenfabricRouter { net };
- let router_item = Router::Openfabric(ofr);
- let frr_word_id = FrrWord::new(fabric_id.to_string())?;
- let router_name = RouterName::Openfabric(frr_word_id.into());
+) -> Result<(ser::RouterName, ser::Router), anyhow::Error> {
+ let ofr = ser::openfabric::OpenfabricRouter { net };
+ let router_item = ser::Router::Openfabric(ofr);
+ let frr_word_id = ser::FrrWord::new(fabric_id.to_string())?;
+ let router_name = ser::RouterName::Openfabric(frr_word_id.into());
Ok((router_name, router_item))
}
/// Helper that builds a OSPF interface from an [`ospf::Area`] and the [`OspfInterfaceProperties`].
fn build_ospf_interface(
- area: ospf::Area,
+ area: ser::ospf::Area,
interface: &OspfInterfaceProperties,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
- let frr_interface = proxmox_frr::ospf::OspfInterface {
+) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
+ let frr_interface = ser::ospf::OspfInterface {
area,
// Interfaces are always none-passive
passive: None,
network_type: if interface.ip.is_some() {
None
} else {
- Some(NetworkType::PointToPoint)
+ Some(ser::ospf::NetworkType::PointToPoint)
},
};
- let interface_name = InterfaceName::Ospf(interface.name.as_str().try_into()?);
+ let interface_name = ser::InterfaceName::Ospf(interface.name.as_str().try_into()?);
Ok((frr_interface.into(), interface_name))
}
/// Helper that builds the OSPF dummy interface using the [`FabricId`] and the [`ospf::Area`].
fn build_ospf_dummy_interface(
fabric_id: &FabricId,
- area: ospf::Area,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
- let frr_interface = proxmox_frr::ospf::OspfInterface {
+ area: ser::ospf::Area,
+) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
+ let frr_interface = ser::ospf::OspfInterface {
area,
passive: Some(true),
network_type: None,
};
- let interface_name = InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
+ let interface_name = ser::InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
Ok((frr_interface.into(), interface_name))
}
@@ -297,9 +301,9 @@ fn build_openfabric_interface(
fabric_config: &OpenfabricProperties,
is_ipv4: bool,
is_ipv6: bool,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
- let frr_word = FrrWord::new(fabric_id.to_string())?;
- let mut frr_interface = proxmox_frr::openfabric::OpenfabricInterface {
+) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
+ let frr_word = ser::FrrWord::new(fabric_id.to_string())?;
+ let mut frr_interface = ser::openfabric::OpenfabricInterface {
fabric_id: frr_word.into(),
// Every interface is not passive by default
passive: None,
@@ -315,7 +319,7 @@ fn build_openfabric_interface(
if frr_interface.hello_interval.is_none() {
frr_interface.hello_interval = fabric_config.hello_interval;
}
- let interface_name = InterfaceName::Openfabric(interface.name.as_str().try_into()?);
+ let interface_name = ser::InterfaceName::Openfabric(interface.name.as_str().try_into()?);
Ok((frr_interface.into(), interface_name))
}
@@ -324,9 +328,9 @@ fn build_openfabric_dummy_interface(
fabric_id: &FabricId,
is_ipv4: bool,
is_ipv6: bool,
-) -> Result<(Interface, InterfaceName), anyhow::Error> {
- let frr_word = FrrWord::new(fabric_id.to_string())?;
- let frr_interface = proxmox_frr::openfabric::OpenfabricInterface {
+) -> Result<(ser::Interface, ser::InterfaceName), anyhow::Error> {
+ let frr_word = ser::FrrWord::new(fabric_id.to_string())?;
+ let frr_interface = ser::openfabric::OpenfabricInterface {
fabric_id: frr_word.into(),
hello_interval: None,
passive: Some(true),
@@ -335,29 +339,37 @@ fn build_openfabric_dummy_interface(
is_ipv4,
is_ipv6,
};
- let interface_name = InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
+ let interface_name = ser::InterfaceName::Openfabric(format!("dummy_{}", fabric_id).try_into()?);
Ok((frr_interface.into(), interface_name))
}
/// Helper that builds a RouteMap for the OpenFabric protocol.
-fn build_openfabric_routemap(fabric_id: &FabricId, router_ip: IpAddr, seq: u32) -> RouteMap {
+fn build_openfabric_routemap(
+ fabric_id: &FabricId,
+ router_ip: IpAddr,
+ seq: u32,
+) -> ser::route_map::RouteMap {
let routemap_name = match router_ip {
- IpAddr::V4(_) => RouteMapName::new("pve_openfabric".to_owned()),
- IpAddr::V6(_) => RouteMapName::new("pve_openfabric6".to_owned()),
+ IpAddr::V4(_) => ser::route_map::RouteMapName::new("pve_openfabric".to_owned()),
+ IpAddr::V6(_) => ser::route_map::RouteMapName::new("pve_openfabric6".to_owned()),
};
- RouteMap {
+ ser::route_map::RouteMap {
name: routemap_name.clone(),
seq,
- action: AccessAction::Permit,
+ action: ser::route_map::AccessAction::Permit,
matches: vec![match router_ip {
- IpAddr::V4(_) => RouteMapMatch::V4(RouteMapMatchInner::IpAddress(AccessListName::new(
- format!("pve_openfabric_{fabric_id}_ips"),
- ))),
- IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::IpAddress(AccessListName::new(
- format!("pve_openfabric_{fabric_id}_ip6s"),
- ))),
+ IpAddr::V4(_) => {
+ ser::route_map::RouteMapMatch::V4(ser::route_map::RouteMapMatchInner::IpAddress(
+ ser::route_map::AccessListName::new(format!("pve_openfabric_{fabric_id}_ips")),
+ ))
+ }
+ IpAddr::V6(_) => {
+ ser::route_map::RouteMapMatch::V6(ser::route_map::RouteMapMatchInner::IpAddress(
+ ser::route_map::AccessListName::new(format!("pve_openfabric_{fabric_id}_ip6s")),
+ ))
+ }
}],
- sets: vec![RouteMapSet::IpSrc(router_ip)],
+ sets: vec![ser::route_map::RouteMapSet::IpSrc(router_ip)],
}
}
@@ -366,17 +378,19 @@ fn build_ospf_dummy_routemap(
fabric_id: &FabricId,
router_ip: Ipv4Addr,
seq: u32,
-) -> Result<RouteMap, anyhow::Error> {
- let routemap_name = RouteMapName::new("pve_ospf".to_owned());
+) -> Result<ser::route_map::RouteMap, anyhow::Error> {
+ let routemap_name = ser::route_map::RouteMapName::new("pve_ospf".to_owned());
// create route-map
- let routemap = RouteMap {
+ let routemap = ser::route_map::RouteMap {
name: routemap_name.clone(),
seq,
- action: AccessAction::Permit,
- matches: vec![RouteMapMatch::V4(RouteMapMatchInner::IpAddress(
- AccessListName::new(format!("pve_ospf_{fabric_id}_ips")),
- ))],
- sets: vec![RouteMapSet::IpSrc(IpAddr::from(router_ip))],
+ action: ser::route_map::AccessAction::Permit,
+ matches: vec![ser::route_map::RouteMapMatch::V4(
+ ser::route_map::RouteMapMatchInner::IpAddress(ser::route_map::AccessListName::new(
+ format!("pve_ospf_{fabric_id}_ips"),
+ )),
+ )],
+ sets: vec![ser::route_map::RouteMapSet::IpSrc(IpAddr::from(router_ip))],
};
Ok(routemap)
diff --git a/proxmox-ve-config/src/sdn/frr.rs b/proxmox-ve-config/src/sdn/frr.rs
index f7929c1..5d4e4b2 100644
--- a/proxmox-ve-config/src/sdn/frr.rs
+++ b/proxmox-ve-config/src/sdn/frr.rs
@@ -1,6 +1,6 @@
use std::collections::{BTreeMap, BTreeSet};
-use proxmox_frr::FrrConfig;
+use proxmox_frr::ser::FrrConfig;
use crate::common::valid::Valid;
use crate::sdn::fabric::{section_config::node::NodeId, FabricConfig};
diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs
index 47bbbeb..09629d4 100644
--- a/proxmox-ve-config/tests/fabric/main.rs
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -1,5 +1,5 @@
#![cfg(feature = "frr")]
-use proxmox_frr::serializer::dump;
+use proxmox_frr::ser::serializer::dump;
use proxmox_ve_config::sdn::{
fabric::{section_config::node::NodeId, FabricConfig},
frr::FrrConfigBuilder,
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs 2/6] frr: add deserialization types for openfabric and ospf
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (2 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 1/6] frr: make room for deserialization structs Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 3/6] ve-config: add helper function to iterate over all nodes in all fabrics Stefan Hanreich
` (30 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
These are used to deserialize `vtysh` command outputs. The output is in
json if the `json` parameter is appended to the command. Currently the
following commands are parsed:
* show openfabric neighbor
* show ip ospf neighbor
* show ip route <protocol>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-frr/Cargo.toml | 1 +
proxmox-frr/debian/control | 4 ++
proxmox-frr/src/de/mod.rs | 56 +++++++++++++++++
proxmox-frr/src/de/openfabric.rs | 101 +++++++++++++++++++++++++++++++
proxmox-frr/src/de/ospf.rs | 64 ++++++++++++++++++++
proxmox-frr/src/lib.rs | 1 +
6 files changed, 227 insertions(+)
create mode 100644 proxmox-frr/src/de/mod.rs
create mode 100644 proxmox-frr/src/de/openfabric.rs
create mode 100644 proxmox-frr/src/de/ospf.rs
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
index 8b01fa4..8ada547 100644
--- a/proxmox-frr/Cargo.toml
+++ b/proxmox-frr/Cargo.toml
@@ -13,6 +13,7 @@ rust-version.workspace = true
thiserror = { workspace = true }
anyhow = "1"
tracing = "0.1"
+serde = { workspace = true, features = [ "derive" ] }
proxmox-network-types = { workspace = true }
proxmox-sdn-types = { workspace = true }
diff --git a/proxmox-frr/debian/control b/proxmox-frr/debian/control
index 9996619..bce0c70 100644
--- a/proxmox-frr/debian/control
+++ b/proxmox-frr/debian/control
@@ -9,6 +9,8 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-proxmox-network-types-0.1+default-dev (>= 0.1.1-~~) <!nocheck>,
librust-proxmox-sdn-types-0.1+default-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>,
librust-thiserror-2+default-dev <!nocheck>,
librust-tracing-0.1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
@@ -27,6 +29,8 @@ Depends:
librust-anyhow-1+default-dev,
librust-proxmox-network-types-0.1+default-dev (>= 0.1.1-~~),
librust-proxmox-sdn-types-0.1+default-dev,
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
librust-thiserror-2+default-dev,
librust-tracing-0.1+default-dev
Provides:
diff --git a/proxmox-frr/src/de/mod.rs b/proxmox-frr/src/de/mod.rs
new file mode 100644
index 0000000..a6674d9
--- /dev/null
+++ b/proxmox-frr/src/de/mod.rs
@@ -0,0 +1,56 @@
+use std::{collections::HashMap, net::IpAddr};
+
+use proxmox_network_types::ip_address::Cidr;
+use serde::{Deserialize, Serialize};
+
+pub mod openfabric;
+pub mod ospf;
+
+/// A nexthop of a route
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct NextHop {
+ #[serde(rename = "interfaceIndex")]
+ pub interface_index: i32,
+ #[serde(rename = "interfaceName")]
+ /// Name of the outgoing interface
+ pub interface_name: String,
+ /// If the nexthop is active
+ pub active: bool,
+ /// If the route has the onlink flag. Onlink means that we pretend that the nexthop is
+ /// directly attached to this link, even if it does not match any interface prefix.
+ #[serde(rename = "onLink")]
+ pub on_link: bool,
+ /// Remap-Source, this rewrites the source address to the following address, if this
+ /// nexthop is used.
+ #[serde(rename = "rmapSource")]
+ pub remap_source: Option<IpAddr>,
+ /// Weight of the nexthop
+ pub weight: i32,
+ /// If this nexthop entry is a duplicate of another (the first one has this unset)
+ pub duplicate: Option<bool>,
+}
+
+/// route
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct Route {
+ /// Array of all the nexthops associated with this route. When you have e.g. two
+ /// connections between two nodes, there is going to be one route, but two nexthops.
+ pub nexthops: Vec<NextHop>,
+ /// Metric of the route
+ pub metric: i32,
+ /// Protocol from which the route originates
+ pub protocol: String,
+ #[serde(rename = "vrfName")]
+ pub vrf_name: String,
+ /// If the route is installed in the kernel routing table
+ pub installed: Option<bool>,
+}
+
+/// Struct to parse zebra routes by FRR.
+///
+/// To get the routes from FRR, instead of asking the daemon of every protocol for their
+/// routes we simply ask zebra which routes have been inserted and filter them by protocol.
+/// The following command is used to accomplish this: `show ip route <protocol> json`.
+/// This struct can be used the deserialize the output of that command.
+#[derive(Debug, Serialize, Deserialize, Default)]
+pub struct Routes(pub HashMap<Cidr, Vec<Route>>);
diff --git a/proxmox-frr/src/de/openfabric.rs b/proxmox-frr/src/de/openfabric.rs
new file mode 100644
index 0000000..837159b
--- /dev/null
+++ b/proxmox-frr/src/de/openfabric.rs
@@ -0,0 +1,101 @@
+use serde::{Deserialize, Serialize};
+
+/// State of the adjacency of a OpenFabric neighbor
+#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
+pub enum AdjacencyState {
+ Initializing,
+ Up,
+ Down,
+ Unknown,
+}
+
+/// Neighbor Interface
+///
+/// Interface used to communicate with a specific neighbor
+#[derive(Debug, Serialize, Deserialize)]
+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, Serialize, Deserialize)]
+pub struct Circuit {
+ /// The hostname of the adjacency peer
+ pub adj: Option<String>,
+ /// The interface of the neighbor
+ pub interface: Option<NeighborInterface>,
+}
+
+/// An openfabric area the same as SDN fabric.
+#[derive(Debug, Serialize, Deserialize)]
+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 openfabric neighbor json'`.
+#[derive(Debug, Serialize, Deserialize, Default)]
+pub struct Neighbors {
+ /// Every sdn fabric is also an openfabric 'area'
+ pub areas: Vec<Area>,
+}
+
+/// The NetworkType of a OpenFabric interface
+#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
+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")]
+ Unknown,
+}
+
+/// The State of a OpenFabric interface
+#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
+pub enum CircuitState {
+ Init,
+ Config,
+ Up,
+ Unknown,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct Interface {
+ pub name: String,
+ pub state: CircuitState,
+ #[serde(rename = "type")]
+ pub ty: NetworkType,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct InterfaceCircuits {
+ pub interface: Interface,
+}
+
+#[derive(Debug, Serialize, Deserialize, Default)]
+pub struct InterfaceArea {
+ pub area: String,
+ pub circuits: Vec<InterfaceCircuits>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Default)]
+pub struct Interfaces {
+ pub areas: Vec<InterfaceArea>,
+}
diff --git a/proxmox-frr/src/de/ospf.rs b/proxmox-frr/src/de/ospf.rs
new file mode 100644
index 0000000..a75609d
--- /dev/null
+++ b/proxmox-frr/src/de/ospf.rs
@@ -0,0 +1,64 @@
+use std::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+
+/// Information about the Neighbor (Peer) of the Adjacency.
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Neighbor {
+ /// The full state of the neighbor. This is "{converged}/{role}".
+ #[serde(rename = "nbrState")]
+ pub neighbor_state: String,
+ /// The uptime of the interface
+ #[serde(rename = "upTime")]
+ pub up_time: String,
+ /// The interface name of this adjacency. This is always a combination of interface
+ /// name and address. e.g. "ens21:5.5.5.3".
+ #[serde(rename = "ifaceName")]
+ pub interface_name: String,
+}
+
+/// The parsed OSPF neighbors
+#[derive(Debug, Deserialize, Default)]
+pub struct Neighbors {
+ /// The OSPF neighbors. This is nearly always a ip-address - neighbor mapping.
+ pub neighbors: HashMap<String, Vec<Neighbor>>,
+}
+
+/// All possible OSPF network-types that can be returned from frr
+#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
+pub enum NetworkType {
+ #[serde(rename = "Null")]
+ Null,
+ #[serde(rename(deserialize = "POINTOPOINT", serialize = "Point-To-Point"))]
+ PointToPoint,
+ #[serde(rename(deserialize = "BROADCAST", serialize = "Broadcast"))]
+ Broadcast,
+ #[serde(rename = "NBMA")]
+ Nbma,
+ #[serde(rename(deserialize = "POINTOMULTIPOINT", serialize = "Point-To-Multipoint"))]
+ PointToMultipoint,
+ #[serde(rename(deserialize = "VIRTUALLINK", serialize = "Virtual Link"))]
+ VirtualLink,
+ #[serde(rename(deserialize = "LOOPBACK", serialize = "Loopback"))]
+ Loopback,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Interface {
+ /// The interface state
+ pub if_up: bool,
+ /// The network type (e.g. point-to-point, broadcast, etc.)
+ ///
+ /// Note there is also a "state" property, but that models the state of the interface (ism),
+ /// which can also be "point-to-point", but it can also be e.g. "Down" or e.g. "DROther"!
+ /// So networkType is the configured network type and state is the state of interface, which
+ /// sometimes is the same as the networkType.
+ pub network_type: NetworkType,
+}
+
+#[derive(Debug, Deserialize, Default)]
+pub struct Interfaces {
+ pub interfaces: HashMap<String, Interface>,
+}
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index 35b62cb..2e6ab62 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -1 +1,2 @@
+pub mod de;
pub mod ser;
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs 3/6] ve-config: add helper function to iterate over all nodes in all fabrics
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (3 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 2/6] frr: add deserialization types for openfabric and ospf Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 4/6] ve-config: add optional tag property to vnet Stefan Hanreich
` (29 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add helper function to iterate over all nodes in all fabrics. This is
especially useful e.g. when retrieving the stats as we need to find all
the fabrics on the current node (and their node config).
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/fabric/mod.rs | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 58a06f9..677a309 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -618,6 +618,11 @@ impl FabricConfig {
.ok_or_else(|| FabricConfigError::FabricDoesNotExist(id.to_string()))
}
+ /// Get an iterator over all the nodes in all fabrics.
+ pub fn all_nodes(&self) -> impl Iterator<Item = (&NodeId, &Node)> + '_ {
+ self.values().flat_map(|entry| entry.nodes())
+ }
+
/// Returns an iterator over mutable references to all [`FabricEntry`] in the config
pub fn get_fabrics_mut(&mut self) -> impl Iterator<Item = &mut FabricEntry> {
self.fabrics.values_mut()
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs 4/6] ve-config: add optional tag property to vnet
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (4 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 3/6] ve-config: add helper function to iterate over all nodes in all fabrics Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 5/6] frr: fix some route deserialization types Stefan Hanreich
` (28 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
When parsing the vnet.cfg config file we also want to get the tag
(vni or vlan). We need this so that we can lookup the vni of the passed
vnet on the status api call. We need the vni so that we can filter the
frr output.
In the future we would probably want to do this better with an enum per
vnet type and parse all possible options.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/config.rs | 27 ++++++++++++++++---
proxmox-ve-config/tests/sdn/main.rs | 5 ++--
.../tests/sdn/resources/running-config.json | 1 +
3 files changed, 28 insertions(+), 5 deletions(-)
diff --git a/proxmox-ve-config/src/sdn/config.rs b/proxmox-ve-config/src/sdn/config.rs
index 031fedc..afc5175 100644
--- a/proxmox-ve-config/src/sdn/config.rs
+++ b/proxmox-ve-config/src/sdn/config.rs
@@ -196,6 +196,7 @@ pub struct SubnetsRunningConfig {
/// Struct for deserializing a vnet entry of the SDN running config
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct VnetRunningConfig {
+ tag: Option<u32>,
zone: ZoneName,
}
@@ -295,14 +296,16 @@ impl SubnetConfig {
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct VnetConfig {
name: VnetName,
+ tag: Option<u32>,
subnets: BTreeMap<Cidr, SubnetConfig>,
}
impl VnetConfig {
- pub fn new(name: VnetName) -> Self {
+ pub fn new(name: VnetName, tag: Option<u32>) -> Self {
Self {
name,
subnets: BTreeMap::default(),
+ tag,
}
}
@@ -310,7 +313,18 @@ impl VnetConfig {
name: VnetName,
subnets: impl IntoIterator<Item = SubnetConfig>,
) -> Result<Self, SdnConfigError> {
- let mut config = Self::new(name);
+ let mut config = Self::new(name, None);
+ config.add_subnets(subnets)?;
+ Ok(config)
+ }
+
+ pub fn from_subnets_and_tag(
+ name: VnetName,
+ tag: Option<u32>,
+ subnets: impl IntoIterator<Item = SubnetConfig>,
+ ) -> Result<Self, SdnConfigError> {
+ let mut config = Self::new(name, None);
+ config.tag = tag;
config.add_subnets(subnets)?;
Ok(config)
}
@@ -342,6 +356,10 @@ impl VnetConfig {
pub fn name(&self) -> &VnetName {
&self.name
}
+
+ pub fn tag(&self) -> &Option<u32> {
+ &self.tag
+ }
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -617,7 +635,10 @@ impl TryFrom<RunningConfig> for SdnConfig {
if let Some(running_vnets) = value.vnets.take() {
for (name, running_config) in running_vnets.ids {
- config.add_vnet(&running_config.zone, VnetConfig::new(name))?;
+ config.add_vnet(
+ &running_config.zone,
+ VnetConfig::new(name, running_config.tag),
+ )?;
}
}
diff --git a/proxmox-ve-config/tests/sdn/main.rs b/proxmox-ve-config/tests/sdn/main.rs
index 94039ad..bd38bbf 100644
--- a/proxmox-ve-config/tests/sdn/main.rs
+++ b/proxmox-ve-config/tests/sdn/main.rs
@@ -25,8 +25,9 @@ fn parse_running_config() {
ZoneName::from_str("zone0").unwrap(),
ZoneType::Simple,
[
- VnetConfig::from_subnets(
+ VnetConfig::from_subnets_and_tag(
VnetName::from_str("vnet0").unwrap(),
+ Some(100),
[
SubnetConfig::new(
SubnetName::from_str("zone0-fd80::-64").unwrap(),
@@ -84,7 +85,7 @@ fn sdn_config() {
let zone0 = ZoneConfig::new(zone0_name.clone(), ZoneType::Qinq);
sdn_config.add_zone(zone0).unwrap();
- let vnet0 = VnetConfig::new(vnet0_name.clone());
+ let vnet0 = VnetConfig::new(vnet0_name.clone(), None);
assert_eq!(
sdn_config.add_vnet(&zone1_name, vnet0.clone()),
Err(SdnConfigError::ZoneNotFound)
diff --git a/proxmox-ve-config/tests/sdn/resources/running-config.json b/proxmox-ve-config/tests/sdn/resources/running-config.json
index b03c20f..d6054ba 100644
--- a/proxmox-ve-config/tests/sdn/resources/running-config.json
+++ b/proxmox-ve-config/tests/sdn/resources/running-config.json
@@ -43,6 +43,7 @@
"ids": {
"vnet0": {
"type": "vnet",
+ "tag": 100,
"zone": "zone0"
},
"vnet1": {
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs 5/6] frr: fix some route deserialization types
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (5 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 4/6] ve-config: add optional tag property to vnet Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 6/6] frr: add deserialization types for EVPN Stefan Hanreich
` (27 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Make some properties optional and add others that aren't always used.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-frr/src/de/mod.rs | 22 +++++++---------------
1 file changed, 7 insertions(+), 15 deletions(-)
diff --git a/proxmox-frr/src/de/mod.rs b/proxmox-frr/src/de/mod.rs
index a6674d9..dd9f058 100644
--- a/proxmox-frr/src/de/mod.rs
+++ b/proxmox-frr/src/de/mod.rs
@@ -9,23 +9,15 @@ pub mod ospf;
/// A nexthop of a route
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NextHop {
- #[serde(rename = "interfaceIndex")]
- pub interface_index: i32,
- #[serde(rename = "interfaceName")]
+ /// IP of the nexthop
+ pub ip: Option<IpAddr>,
/// Name of the outgoing interface
- pub interface_name: String,
+ #[serde(rename = "interfaceName")]
+ pub interface_name: Option<String>,
/// If the nexthop is active
- pub active: bool,
- /// If the route has the onlink flag. Onlink means that we pretend that the nexthop is
- /// directly attached to this link, even if it does not match any interface prefix.
- #[serde(rename = "onLink")]
- pub on_link: bool,
- /// Remap-Source, this rewrites the source address to the following address, if this
- /// nexthop is used.
- #[serde(rename = "rmapSource")]
- pub remap_source: Option<IpAddr>,
- /// Weight of the nexthop
- pub weight: i32,
+ pub active: Option<bool>,
+ /// If this nexthop entry is reachable from this host
+ pub unreachable: Option<bool>,
/// If this nexthop entry is a duplicate of another (the first one has this unset)
pub duplicate: Option<bool>,
}
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs 6/6] frr: add deserialization types for EVPN
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (6 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 5/6] frr: fix some route deserialization types Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 01/10] pve-rs: firewall: cargo: fmt Stefan Hanreich
` (26 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add deserialization types for the `show bgp l2vpn evpn route vni <vni>`
command. This command shows all the L2VPN (EVPN) routes that are
distributed over BGP.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-frr/Cargo.toml | 1 +
proxmox-frr/debian/control | 2 +
proxmox-frr/src/de/evpn.rs | 165 +++++++++++++++++++++++++++++++++++++
proxmox-frr/src/de/mod.rs | 1 +
4 files changed, 169 insertions(+)
create mode 100644 proxmox-frr/src/de/evpn.rs
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
index 8ada547..1b241a0 100644
--- a/proxmox-frr/Cargo.toml
+++ b/proxmox-frr/Cargo.toml
@@ -14,6 +14,7 @@ thiserror = { workspace = true }
anyhow = "1"
tracing = "0.1"
serde = { workspace = true, features = [ "derive" ] }
+serde_repr = "0.1"
proxmox-network-types = { workspace = true }
proxmox-sdn-types = { workspace = true }
diff --git a/proxmox-frr/debian/control b/proxmox-frr/debian/control
index bce0c70..3a73732 100644
--- a/proxmox-frr/debian/control
+++ b/proxmox-frr/debian/control
@@ -11,6 +11,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-proxmox-sdn-types-0.1+default-dev <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>,
+ librust-serde-repr-0.1+default-dev <!nocheck>,
librust-thiserror-2+default-dev <!nocheck>,
librust-tracing-0.1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
@@ -31,6 +32,7 @@ Depends:
librust-proxmox-sdn-types-0.1+default-dev,
librust-serde-1+default-dev,
librust-serde-1+derive-dev,
+ librust-serde-repr-0.1+default-dev,
librust-thiserror-2+default-dev,
librust-tracing-0.1+default-dev
Provides:
diff --git a/proxmox-frr/src/de/evpn.rs b/proxmox-frr/src/de/evpn.rs
new file mode 100644
index 0000000..97faca4
--- /dev/null
+++ b/proxmox-frr/src/de/evpn.rs
@@ -0,0 +1,165 @@
+use std::{collections::HashMap, net::IpAddr};
+
+use proxmox_network_types::mac_address::MacAddress;
+use serde::Deserialize;
+use serde_repr::Deserialize_repr;
+
+/// All EVPN routes
+#[derive(Debug, Default, Deserialize)]
+pub struct Routes(pub HashMap<String, Entry>);
+
+/// The evpn routes a stored in a hashtable, which has a numPrefix and numPath key at
+/// the end which stores the number of paths and prefixes. These two keys have a i32
+/// value, while the other entries have a normal [`Route`] entry.
+#[derive(Debug, Deserialize)]
+#[serde(untagged)]
+pub enum Entry {
+ /// Route
+ Route(Route),
+ // This stores the numPrefix and numPath properties (which are not used) (this is
+ // a workaround)
+ Metadata(i32),
+}
+
+/// An EVPN route
+#[derive(Debug, Deserialize)]
+pub struct Route {
+ /// The full EVPN prefix
+ pub prefix: String,
+ /// Length of the prefix
+ #[serde(rename = "prefixLen")]
+ pub prefix_len: i32,
+ /// Paths to the EVPN route
+ pub paths: Vec<Vec<Path>>,
+}
+
+/// An EVPN Route Path
+#[derive(Debug, Deserialize)]
+pub struct Path {
+ /// Is this path valid
+ pub valid: bool,
+ /// Is this the best path
+ pub bestpath: Option<bool>,
+ /// Reason for selection (longer explanatory string)
+ #[serde(rename = "selectionReason")]
+ pub selection_reason: Option<String>,
+ /// From where the EVPN Route path comes
+ #[serde(rename = "pathFrom")]
+ pub path_from: PathFrom,
+ /// EVPN route type
+ #[serde(rename = "routeType")]
+ pub route_type: RouteType,
+ /// Ethernet tag
+ #[serde(rename = "ethTag")]
+ pub ethernet_tag: i32,
+ /// Mac Address length
+ #[serde(rename = "macLen")]
+ pub mac_length: Option<i32>,
+ /// Mac Address
+ pub mac: Option<MacAddress>,
+ /// IP Address lenght
+ #[serde(rename = "ipLen")]
+ pub ip_length: Option<i32>,
+ /// IP Address
+ pub ip: Option<IpAddr>,
+ /// Local Preference of the path
+ #[serde(rename = "locPrf")]
+ pub local_preference: Option<i32>,
+ /// Weight of the path
+ pub weight: i32,
+ /// PeerId, can be either IP or unspecified
+ #[serde(rename = "peerId")]
+ pub peer_id: PeerId,
+ /// AS path of the EVPN route
+ #[serde(rename = "path")]
+ pub as_path: String,
+ /// Origin of the route
+ pub origin: Origin,
+ /// Extended BGP Community
+ #[serde(rename = "extendedCommunity")]
+ pub extended_community: ExtendedCommunity,
+ /// Nexthops
+ pub nexthops: Vec<Nexthop>,
+}
+
+/// PeerId of the EVPN route path
+#[derive(Debug, Deserialize)]
+#[serde(untagged)]
+pub enum PeerId {
+ /// IP Address
+ IpAddr(IpAddr),
+ /// Not specified
+ Unspec(String),
+}
+
+/// Nexthop of a EVPN path
+#[derive(Debug, Deserialize)]
+pub struct Nexthop {
+ /// IP of the nexthop
+ pub ip: IpAddr,
+ /// Hostname of the nexthop
+ pub hostname: String,
+ /// Afi of the ip
+ pub afi: Option<Protocol>,
+ /// Used
+ pub used: bool,
+}
+
+/// Protocol AFI for a EVPN nexthop
+#[derive(Debug, Deserialize)]
+pub enum Protocol {
+ /// IPV4
+ #[serde(rename = "ipv4")]
+ IPv4,
+ /// IPV6
+ #[serde(rename = "ipv6")]
+ IPv6,
+}
+
+/// Extended Community for EVPN route
+#[derive(Debug, Deserialize)]
+pub struct ExtendedCommunity {
+ /// String with all the BGP ExtendedCommunities (this also contains the
+ /// RouteTarget)
+ pub string: String,
+}
+
+/// Origin of the EVPN route
+#[derive(Debug, Deserialize)]
+pub enum Origin {
+ /// Interior Gateway Protocol
+ #[serde(rename = "IGP")]
+ Igp,
+ #[serde(rename = "EGP")]
+ /// Exterior Gateway Protocol
+ Egp,
+ #[serde(rename = "incomplete")]
+ /// Incomplete
+ Incomplete,
+}
+
+/// EVPN RouteType
+#[derive(Debug, Deserialize_repr)]
+#[repr(u8)]
+pub enum RouteType {
+ /// EthernetAutoDiscovery
+ EthernetAutoDiscovery = 1,
+ /// MacIpAdvertisement
+ MacIpAdvertisement = 2,
+ /// InclusiveMulticastEthernetTag
+ InclusiveMulticastEthernetTag = 3,
+ /// EthernetSegment
+ EthernetSegment = 4,
+ /// IpPrefix
+ IpPrefix = 5,
+}
+
+/// From where the EVPN route path comes
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum PathFrom {
+ /// Internal
+ Internal,
+ /// External
+ External,
+}
diff --git a/proxmox-frr/src/de/mod.rs b/proxmox-frr/src/de/mod.rs
index dd9f058..121451b 100644
--- a/proxmox-frr/src/de/mod.rs
+++ b/proxmox-frr/src/de/mod.rs
@@ -3,6 +3,7 @@ use std::{collections::HashMap, net::IpAddr};
use proxmox_network_types::ip_address::Cidr;
use serde::{Deserialize, Serialize};
+pub mod evpn;
pub mod openfabric;
pub mod ospf;
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs 01/10] pve-rs: firewall: cargo: fmt
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (7 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 6/6] frr: add deserialization types for EVPN Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 02/10] pve-rs: firewall: add missing documentation comments Stefan Hanreich
` (25 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Run cargo fmt.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/firewall/sdn.rs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/pve-rs/src/bindings/firewall/sdn.rs b/pve-rs/src/bindings/firewall/sdn.rs
index 4e8af7c..6a773cc 100644
--- a/pve-rs/src/bindings/firewall/sdn.rs
+++ b/pve-rs/src/bindings/firewall/sdn.rs
@@ -8,18 +8,18 @@ pub mod pve_rs_firewall_sdn {
use std::collections::HashMap;
use std::{fs, io};
- use anyhow::{bail, Context, Error};
+ use anyhow::{Context, Error, bail};
use serde::Serialize;
use proxmox_ve_config::{
common::Allowlist,
- firewall::types::ipset::{IpsetAddress, IpsetEntry},
firewall::types::Ipset,
+ firewall::types::ipset::{IpsetAddress, IpsetEntry},
guest::types::Vmid,
sdn::{
+ VnetName,
config::{RunningConfig, SdnConfig},
ipam::{Ipam, IpamJson},
- VnetName,
},
};
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs 02/10] pve-rs: firewall: add missing documentation comments
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (8 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 01/10] pve-rs: firewall: cargo: fmt Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 03/10] pve-rs: cargo: bump proxmox-apt and proxmox-ve-config versions Stefan Hanreich
` (24 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
Add missing documentation comments on the SdnFirewallConfig struct and
the config function.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/firewall/sdn.rs | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/pve-rs/src/bindings/firewall/sdn.rs b/pve-rs/src/bindings/firewall/sdn.rs
index 6a773cc..530e805 100644
--- a/pve-rs/src/bindings/firewall/sdn.rs
+++ b/pve-rs/src/bindings/firewall/sdn.rs
@@ -62,6 +62,11 @@ pub mod pve_rs_firewall_sdn {
}
}
+ /// A struct holding the SDN part of a firewall configuration.
+ ///
+ /// The main use is for returning the auto-generated SDN ipsets to Perl, so they can be
+ /// included in the API response as well as used in pve-firewall for generating the respective
+ /// ipsets.
#[derive(Clone, Debug, Default, Serialize)]
pub struct SdnFirewallConfig {
ipset: HashMap<String, Vec<LegacyIpsetEntry>>,
@@ -94,6 +99,11 @@ pub mod pve_rs_firewall_sdn {
const SDN_IPAM: &str = "/etc/pve/sdn/pve-ipam-state.json";
const SDN_IPAM_LEGACY: &str = "/etc/pve/priv/ipam.db"; // TODO: remove with PVE 9+
+ /// Generate the SDN firewall configuration.
+ ///
+ /// vnet_filter and vm_filter can be passed by perl in order to filter the returned IPsets to
+ /// only include the specified vnets / vms. This is necessary for ACL, since it is currently
+ /// not implemented for Proxmox VE in Rust.
#[export]
pub fn config(
vnet_filter: Option<Vec<VnetName>>,
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs 03/10] pve-rs: cargo: bump proxmox-apt and proxmox-ve-config versions
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (9 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 02/10] pve-rs: firewall: add missing documentation comments Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 04/10] pve-rs: fabrics: update proxmox-frr import path Stefan Hanreich
` (23 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Bump the proxmox-apt and proxmox-ve-config crate versions.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/Cargo.toml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index e40c55c..242c9f7 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -29,7 +29,7 @@ url = "2"
perlmod = { version = "0.14", features = ["exporter"] }
-proxmox-apt = { version = "0.99.2", features = ["cache"] }
+proxmox-apt = { version = "0.99.4", features = ["cache"] }
proxmox-apt-api-types = "2"
proxmox-base64 = "1"
proxmox-config-digest = "1"
@@ -47,7 +47,7 @@ proxmox-subscription = "1"
proxmox-sys = "1"
proxmox-tfa = { version = "6.0.3", features = ["api"] }
proxmox-time = "2"
-proxmox-ve-config = { version = "0.4.2", features = [ "frr" ] }
+proxmox-ve-config = { version = "0.4.4", features = [ "frr" ] }
# [patch.crates-io]
# pbs-api-types = { path = "../../proxmox/pbs-api-types" }
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs 04/10] pve-rs: fabrics: update proxmox-frr import path
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (10 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 03/10] pve-rs: cargo: bump proxmox-apt and proxmox-ve-config versions Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 05/10] pve-rs: fabrics: fix clippy lint warnings Stefan Hanreich
` (22 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Update the proxmox-frr import path to the new `ser` and `de` subfolders.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 9d5fa6c..376527f 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -16,7 +16,7 @@ pub mod pve_rs_sdn_fabrics {
use serde::{Deserialize, Serialize};
use perlmod::Value;
- use proxmox_frr::serializer::to_raw_config;
+ use proxmox_frr::ser::serializer::to_raw_config;
use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
use proxmox_section_config::typed::SectionConfigData;
use proxmox_ve_config::common::valid::Validatable;
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs 05/10] pve-rs: fabrics: fix clippy lint warnings
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (11 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 04/10] pve-rs: fabrics: update proxmox-frr import path Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 06/10] pve-rs: fabrics: add function to get status of fabric Stefan Hanreich
` (21 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Fix some clippy lint warnings: unneeded '&'.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 376527f..c85c411 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -348,14 +348,14 @@ pub mod pve_rs_sdn_fabrics {
match node {
ConfigNode::Openfabric(node_section) => {
for interface in node_section.properties_mut().interfaces_mut() {
- if let Some(mapped_name) = map_name(&mapping, &interface.name())? {
+ 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())? {
+ if let Some(mapped_name) = map_name(&mapping, interface.name())? {
interface.set_name(mapped_name);
}
}
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs 06/10] pve-rs: fabrics: add function to get status of fabric
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (12 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 05/10] pve-rs: fabrics: fix clippy lint warnings Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 07/10] pve-rs: fabrics: add function to get l2vpn and l3vpn routes for evpn Stefan Hanreich
` (20 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add a function to get the status of a fabric. This is the status which
will then be inserted into the pvestatd daemon and returned through the
resources api. In order the generate the HashMap of statuses for all
fabrics we need to read the fabric config and execute a vtysh (frr)
command to get the routes of the corresponding fabric. If there is at
least one route which is related to the fabric, the fabric is considered
"ok".
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 79 +++++++++++++++-
pve-rs/src/lib.rs | 2 +
pve-rs/src/sdn/mod.rs | 3 +
pve-rs/src/sdn/status.rs | 140 +++++++++++++++++++++++++++++
4 files changed, 222 insertions(+), 2 deletions(-)
create mode 100644 pve-rs/src/sdn/mod.rs
create mode 100644 pve-rs/src/sdn/status.rs
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index c85c411..dcd5bcc 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -9,9 +9,10 @@ pub mod pve_rs_sdn_fabrics {
use std::fmt::Write;
use std::net::IpAddr;
use std::ops::Deref;
+ use std::process::Command;
use std::sync::Mutex;
- use anyhow::Error;
+ use anyhow::{Context, Error};
use openssl::hash::{MessageDigest, hash};
use serde::{Deserialize, Serialize};
@@ -19,7 +20,7 @@ pub mod pve_rs_sdn_fabrics {
use proxmox_frr::ser::serializer::to_raw_config;
use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
use proxmox_section_config::typed::SectionConfigData;
- use proxmox_ve_config::common::valid::Validatable;
+ use proxmox_ve_config::common::valid::{Valid, Validatable};
use proxmox_ve_config::sdn::fabric::section_config::Section;
use proxmox_ve_config::sdn::fabric::section_config::fabric::{
@@ -34,6 +35,8 @@ pub mod pve_rs_sdn_fabrics {
use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry};
use proxmox_ve_config::sdn::frr::FrrConfigBuilder;
+ use crate::sdn::status::{self, RunningConfig};
+
/// A SDN Fabric config instance.
#[derive(Serialize, Deserialize)]
pub struct PerlFabricConfig {
@@ -587,4 +590,76 @@ pub mod pve_rs_sdn_fabrics {
Ok(interfaces)
}
+
+ /// Read and parse the running-config and get the fabrics section
+ ///
+ /// This will return a valid FabricConfig. Note that we read the file manually and not through
+ /// the cluster filesystem as with perl, so this will be slower.
+ fn get_fabrics_config() -> Result<Valid<FabricConfig>, anyhow::Error> {
+ let raw_config = std::fs::read_to_string("/etc/pve/sdn/.running-config")?;
+ let running_config: RunningConfig =
+ serde_json::from_str(&raw_config).with_context(|| "error parsing running-config")?;
+ let section_config = SectionConfigData::from_iter(running_config.fabrics.ids);
+ FabricConfig::from_section_config(section_config)
+ .with_context(|| "error converting section config to fabricconfig")
+ }
+
+ /// Return the status of all fabrics on this node.
+ ///
+ /// Go through all fabrics in the config, then filter out the ones that exist on this node.
+ /// Check if there are any routes in the routing table that use the interface specified in the
+ /// config. If there are, show "ok" as status, otherwise "not ok".
+ #[export]
+ fn status() -> Result<HashMap<FabricId, status::Status>, Error> {
+ let openfabric_ipv4_routes_string = String::from_utf8(
+ Command::new("sh")
+ .args(["-c", "vtysh -c 'show ip route openfabric json'"])
+ .output()?
+ .stdout,
+ )?;
+
+ let openfabric_ipv6_routes_string = String::from_utf8(
+ Command::new("sh")
+ .args(["-c", "vtysh -c 'show ipv6 route openfabric json'"])
+ .output()?
+ .stdout,
+ )?;
+
+ let ospf_routes_string = String::from_utf8(
+ Command::new("sh")
+ .args(["-c", "vtysh -c 'show ip route ospf json'"])
+ .output()?
+ .stdout,
+ )?;
+
+ let mut openfabric_routes: proxmox_frr::de::Routes =
+ if openfabric_ipv4_routes_string.is_empty() {
+ proxmox_frr::de::Routes::default()
+ } else {
+ serde_json::from_str(&openfabric_ipv4_routes_string)
+ .with_context(|| "error parsing openfabric ipv4 routes")?
+ };
+ if !openfabric_ipv6_routes_string.is_empty() {
+ let openfabric_ipv6_routes: proxmox_frr::de::Routes =
+ serde_json::from_str(&openfabric_ipv6_routes_string)
+ .with_context(|| "error parsing openfabric ipv6 routes")?;
+ openfabric_routes.0.extend(openfabric_ipv6_routes.0);
+ }
+
+ let ospf_routes: proxmox_frr::de::Routes = if ospf_routes_string.is_empty() {
+ proxmox_frr::de::Routes::default()
+ } else {
+ serde_json::from_str(&ospf_routes_string)
+ .with_context(|| "error parsing ospf routes")?
+ };
+
+ let config = get_fabrics_config()?;
+
+ let route_status = status::RoutesParsed {
+ openfabric: openfabric_routes,
+ ospf: ospf_routes,
+ };
+
+ status::get_status(config, route_status)
+ }
}
diff --git a/pve-rs/src/lib.rs b/pve-rs/src/lib.rs
index b2fcaae..b32b061 100644
--- a/pve-rs/src/lib.rs
+++ b/pve-rs/src/lib.rs
@@ -13,6 +13,8 @@ use proxmox_notify::{Config, Notification, Severity};
#[path = "../common/src/mod.rs"]
mod common;
+mod sdn;
+
pub mod bindings;
fn send_notification(notification: &Notification) -> Result<(), Error> {
diff --git a/pve-rs/src/sdn/mod.rs b/pve-rs/src/sdn/mod.rs
new file mode 100644
index 0000000..f4a42fa
--- /dev/null
+++ b/pve-rs/src/sdn/mod.rs
@@ -0,0 +1,3 @@
+/// This module contains status-related structs that represent Routes, Neighbors and general Status
+/// for all Protocols and SDN Objects
+pub mod status;
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
new file mode 100644
index 0000000..c04a0c1
--- /dev/null
+++ b/pve-rs/src/sdn/status.rs
@@ -0,0 +1,140 @@
+use std::collections::{BTreeMap, HashMap, HashSet};
+
+use proxmox_section_config::typed::SectionConfigData;
+use serde::{Deserialize, Serialize};
+
+use proxmox_frr::de::{self};
+use proxmox_ve_config::{
+ common::valid::Valid,
+ sdn::fabric::{
+ FabricConfig,
+ section_config::{Section, fabric::FabricId, node::Node as ConfigNode},
+ },
+};
+
+/// Protocol
+#[derive(Debug, Serialize, Clone, Copy)]
+#[serde(rename_all = "lowercase")]
+pub enum Protocol {
+ /// Openfabric
+ Openfabric,
+ /// OSPF
+ Ospf,
+}
+
+/// The status of a fabric.
+#[derive(Debug, Serialize)]
+pub enum FabricStatus {
+ /// The fabric exists and has a route
+ #[serde(rename = "ok")]
+ Ok,
+ /// The fabric does not exist or doesn't distribute any routes
+ #[serde(rename = "not ok")]
+ NotOk,
+}
+
+/// Status of a fabric.
+///
+/// Models the current state of the fabric, the status is determined by checking if any
+/// routes are propagated. This will be inserted into the PVE resources.
+#[derive(Debug, Serialize)]
+pub struct Status {
+ #[serde(rename = "type")]
+ ty: String,
+ status: FabricStatus,
+ protocol: Protocol,
+ network: FabricId,
+ network_type: String,
+}
+
+/// Parsed routes for all protocols
+///
+/// These are the routes parsed from the json output of:
+/// `vtysh -c 'show ip route <protocol> json'`.
+#[derive(Debug, Serialize)]
+pub struct RoutesParsed {
+ /// All openfabric routes in FRR
+ pub openfabric: de::Routes,
+ /// All ospf routes in FRR
+ pub ospf: de::Routes,
+}
+
+/// Config used to parse the fabric part of the running-config
+#[derive(Deserialize)]
+pub struct RunningConfig {
+ pub fabrics: FabricsRunningConfig,
+}
+
+/// Map of ids for all the fabrics in the running-config
+#[derive(Deserialize)]
+pub struct FabricsRunningConfig {
+ pub ids: BTreeMap<String, Section>,
+}
+
+/// Get the status for each fabric using the parsed routes from frr
+///
+/// Using the parsed routes we get from frr, filter and map them to a HashMap mapping every
+/// fabric to a status struct containing basic info about the fabric and the status (if it
+/// propagates a route).
+pub fn get_status(
+ config: Valid<FabricConfig>,
+ routes: RoutesParsed,
+) -> Result<HashMap<FabricId, Status>, anyhow::Error> {
+ let hostname = proxmox_sys::nodename();
+
+ let mut stats: HashMap<FabricId, Status> = HashMap::new();
+
+ for (nodeid, node) in config.all_nodes() {
+ if nodeid.as_str() != hostname {
+ continue;
+ }
+ let fabric_id = node.id().fabric_id();
+
+ let (current_protocol, all_routes) = match &node {
+ ConfigNode::Openfabric(_) => (Protocol::Openfabric, &routes.openfabric.0),
+ ConfigNode::Ospf(_) => (Protocol::Ospf, &routes.ospf.0),
+ };
+
+ // get interfaces
+ let interface_names: HashSet<&str> = match node {
+ ConfigNode::Openfabric(n) => n
+ .properties()
+ .interfaces()
+ .map(|i| i.name().as_str())
+ .collect(),
+ ConfigNode::Ospf(n) => n
+ .properties()
+ .interfaces()
+ .map(|i| i.name().as_str())
+ .collect(),
+ };
+
+ // determine status by checking if any routes exist for our interfaces
+ let has_routes = all_routes.values().any(|v| {
+ v.iter().any(|route| {
+ route.nexthops.iter().any(|nexthop| {
+ if let Some(iface_name) = &nexthop.interface_name {
+ interface_names.contains(iface_name.as_str())
+ } else {
+ false
+ }
+ })
+ })
+ });
+
+ let fabric = Status {
+ ty: "network".to_owned(),
+ status: if has_routes {
+ FabricStatus::Ok
+ } else {
+ FabricStatus::NotOk
+ },
+ protocol: current_protocol,
+ network: fabric_id.clone(),
+ network_type: "fabric".to_string(),
+ };
+ stats.insert(fabric_id.clone(), fabric);
+ }
+
+ Ok(stats)
+}
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs 07/10] pve-rs: fabrics: add function to get l2vpn and l3vpn routes for evpn
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (13 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 06/10] pve-rs: fabrics: add function to get status of fabric Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 08/10] pve-rs: fabrics: add function to get routes learned by a fabric Stefan Hanreich
` (19 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add functions to get the l3vpn and l2vpn routes from frr so that we can
display them in the ui in the evpn zone content view. L3vpn route
retrieval is quite easy, we just get the routes that are in a specific
vrf (the vrf of the zone) (we could do this with iproute2, but we use
vtysh so that we can get all nexthops). For l2vpn we could also use
`bridge fdb`, but then we don't get the VNI and the ip address
associated to the l2vpn route distributed by EVPN. In order to get all
the information we show all the type2 routes that EVPN receives and get
the mac and ip address of them. We also filter by installed and bestpath
so we only display the installed and best routes.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 53 ++++++++++++++++-
pve-rs/src/sdn/status.rs | 93 +++++++++++++++++++++++++++++-
2 files changed, 144 insertions(+), 2 deletions(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index dcd5bcc..a1f056d 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -12,7 +12,7 @@ pub mod pve_rs_sdn_fabrics {
use std::process::Command;
use std::sync::Mutex;
- use anyhow::{Context, Error};
+ use anyhow::{Context, Error, format_err};
use openssl::hash::{MessageDigest, hash};
use serde::{Deserialize, Serialize};
@@ -22,6 +22,7 @@ pub mod pve_rs_sdn_fabrics {
use proxmox_section_config::typed::SectionConfigData;
use proxmox_ve_config::common::valid::{Valid, Validatable};
+ use proxmox_ve_config::sdn::config::{SdnConfig, ZoneConfig};
use proxmox_ve_config::sdn::fabric::section_config::Section;
use proxmox_ve_config::sdn::fabric::section_config::fabric::{
Fabric as ConfigFabric, FabricId,
@@ -662,4 +663,54 @@ pub mod pve_rs_sdn_fabrics {
status::get_status(config, route_status)
}
+
+ /// Get all the L3 routes for the passed zone.
+ ///
+ /// Every zone has a vrf named `vrf_{zone}`. Show all the L3 (IP) routes on the VRF of the
+ /// zone.
+ #[export]
+ fn l3vpn_routes(zone: String) -> Result<status::L3VPNRoutes, Error> {
+ let command = format!("vtysh -c 'show ip route vrf vrf_{zone} json'");
+ let l3vpn_routes_string =
+ String::from_utf8(Command::new("sh").args(["-c", &command]).output()?.stdout)?;
+ let l3vpn_routes: proxmox_frr::de::Routes = if l3vpn_routes_string.is_empty() {
+ proxmox_frr::de::Routes::default()
+ } else {
+ serde_json::from_str(&l3vpn_routes_string)
+ .with_context(|| "error parsing l3vpn routes")?
+ };
+
+ status::get_l3vpn_routes(&format!("vrf_{zone}"), l3vpn_routes)
+ }
+
+ /// Get all the L2 routes for the passed vnet.
+ ///
+ /// When using VXLAN the vnet "stores" the L2 routes in it's FDB. The best way to retrieve them
+ /// with additional metadata is to query FRR. Use the `show bgp l2vpn evpn route` command.
+ /// To filter by vnet, get the VNI of the vnet from the config and use it in the command.
+ #[export]
+ fn l2vpn_routes(vnet: String) -> Result<status::L2VPNRoutes, Error> {
+ // read config to get the vni of the vnet
+ let raw_config = std::fs::read_to_string("/etc/pve/sdn/.running-config")?;
+ let running_config: proxmox_ve_config::sdn::config::RunningConfig =
+ serde_json::from_str(&raw_config)?;
+ let parsed_config = SdnConfig::try_from(running_config)?;
+
+ let vni = parsed_config
+ .zones()
+ .flat_map(ZoneConfig::vnets)
+ .find(|vnet_config| vnet_config.name().as_ref() == vnet)
+ .ok_or_else(|| format_err!("could not find vnet {vnet}"))?
+ .tag()
+ .ok_or_else(|| format_err!("vnet {vnet} has no tag"))?;
+
+ let command = format!("vtysh -c 'show bgp l2vpn evpn route vni {vni} type 2 json'");
+ let l2vpn_routes_string =
+ String::from_utf8(Command::new("sh").args(["-c", &command]).output()?.stdout)?;
+
+ let routes = serde_json::from_str(&l2vpn_routes_string)
+ .with_context(|| "error parsing l2vpn routes")?;
+
+ status::get_l2vpn_routes(routes)
+ }
}
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index c04a0c1..0c9dc0f 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -1,6 +1,8 @@
use std::collections::{BTreeMap, HashMap, HashSet};
+use std::net::IpAddr;
-use proxmox_section_config::typed::SectionConfigData;
+use proxmox_network_types::ip_address::Cidr;
+use proxmox_network_types::mac_address::MacAddress;
use serde::{Deserialize, Serialize};
use proxmox_frr::de::{self};
@@ -138,3 +140,92 @@ pub fn get_status(
Ok(stats)
}
+/// Common for nexthops, they can be either a interface name or a ip addr
+#[derive(Debug, Serialize)]
+#[serde(untagged)]
+pub enum IpAddrOrInterfaceName {
+ /// IpAddr
+ IpAddr(IpAddr),
+ /// Interface Name
+ InterfaceName(String),
+}
+
+/// One L3VPN route
+#[derive(Debug, Serialize)]
+pub struct L3VPNRoute {
+ ip: Cidr,
+ protocol: String,
+ metric: i32,
+ nexthops: Vec<IpAddrOrInterfaceName>,
+}
+
+/// All L3VPN routes of a zone
+#[derive(Debug, Serialize)]
+pub struct L3VPNRoutes(Vec<L3VPNRoute>);
+
+/// Convert parsed routes from frr into l3vpn routes, this means we need to match against the vrf
+/// name of the zone.
+pub fn get_l3vpn_routes(vrf: &str, routes: de::Routes) -> Result<L3VPNRoutes, anyhow::Error> {
+ let mut result = Vec::new();
+ for (prefix, routes) in routes.0 {
+ for route in routes {
+ if route.vrf_name == vrf && route.installed.unwrap_or_default() {
+ result.push(L3VPNRoute {
+ ip: prefix,
+ metric: route.metric,
+ protocol: route.protocol,
+ nexthops: route
+ .nexthops
+ .into_iter()
+ .filter_map(|nh| {
+ if nh.duplicate.unwrap_or_default() {
+ return None;
+ }
+
+ nh.ip.map(IpAddrOrInterfaceName::IpAddr).or_else(|| {
+ nh.interface_name.map(IpAddrOrInterfaceName::InterfaceName)
+ })
+ })
+ .collect(),
+ });
+ }
+ }
+ }
+ Ok(L3VPNRoutes(result))
+}
+
+/// One L2VPN route
+#[derive(Debug, Serialize)]
+pub struct L2VPNRoute {
+ mac: MacAddress,
+ ip: IpAddr,
+ nexthop: IpAddr,
+}
+
+/// All L2VPN routes of a specific vnet
+#[derive(Debug, Serialize)]
+pub struct L2VPNRoutes(Vec<L2VPNRoute>);
+
+/// Convert the parsed frr evpn struct into an array of structured L2VPN routes
+pub fn get_l2vpn_routes(routes: de::evpn::Routes) -> Result<L2VPNRoutes, anyhow::Error> {
+ let mut result = Vec::new();
+ for route in routes.0.values() {
+ if let de::evpn::Entry::Route(r) = route {
+ r.paths.iter().flatten().for_each(|path| {
+ if path.bestpath.unwrap_or_default() {
+ if let (Some(mac), Some(ip), Some(nh)) =
+ (path.mac, path.ip, path.nexthops.first())
+ {
+ result.push(L2VPNRoute {
+ mac,
+ ip,
+ nexthop: nh.ip,
+ });
+ }
+ }
+ });
+ }
+ }
+
+ Ok(L2VPNRoutes(result))
+}
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs 08/10] pve-rs: fabrics: add function to get routes learned by a fabric
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (14 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 07/10] pve-rs: fabrics: add function to get l2vpn and l3vpn routes for evpn Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 09/10] pve-rs: fabrics: add function to get the interfaces used for " Stefan Hanreich
` (18 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add function to retrieve routes learned via OpenFabric or OSPF for a
specific fabric. Query FRR using `show ip route <protocol>` commands so
that we get a common json schema for every protocol. Match routes to the
fabric by comparing outgoing interfaces against the fabric's configured
interfaces on the local node.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 61 +++++++++++++++++++
pve-rs/src/sdn/status.rs | 94 +++++++++++++++++++++++++++++-
2 files changed, 154 insertions(+), 1 deletion(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index a1f056d..5fbb67e 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -605,6 +605,67 @@ pub mod pve_rs_sdn_fabrics {
.with_context(|| "error converting section config to fabricconfig")
}
+ /// Get the routes that have been learned and distributed by this specific fabric on this node.
+ ///
+ /// Read and parse the fabric config to get the protocol and the interfaces. Parse the vtysh
+ /// output and assign the routes to a fabric by using the interface list. Return a list of
+ /// common route structs.
+ #[export]
+ fn routes(fabric_id: FabricId) -> Result<Vec<status::RouteStatus>, Error> {
+ // Read fabric config to get protocol of fabric
+ let config = get_fabrics_config()?;
+
+ let fabric = config.get_fabric(&fabric_id)?;
+ match fabric {
+ FabricEntry::Openfabric(_) => {
+ let openfabric_ipv4_routes_string = String::from_utf8(
+ Command::new("sh")
+ .args(["-c", "vtysh -c 'show ip route openfabric json'"])
+ .output()?
+ .stdout,
+ )?;
+
+ let openfabric_ipv6_routes_string = String::from_utf8(
+ Command::new("sh")
+ .args(["-c", "vtysh -c 'show ipv6 route openfabric json'"])
+ .output()?
+ .stdout,
+ )?;
+
+ let mut openfabric_routes: proxmox_frr::de::Routes =
+ if openfabric_ipv4_routes_string.is_empty() {
+ proxmox_frr::de::Routes::default()
+ } else {
+ serde_json::from_str(&openfabric_ipv4_routes_string)
+ .with_context(|| "error parsing openfabric ipv4 routes")?
+ };
+ if !openfabric_ipv6_routes_string.is_empty() {
+ let openfabric_ipv6_routes: proxmox_frr::de::Routes =
+ serde_json::from_str(&openfabric_ipv6_routes_string)
+ .with_context(|| "error parsing openfabric ipv6 routes")?;
+ openfabric_routes.0.extend(openfabric_ipv6_routes.0);
+ }
+ status::get_routes(fabric_id, config, openfabric_routes)
+ }
+ FabricEntry::Ospf(_) => {
+ let ospf_routes_string = String::from_utf8(
+ Command::new("sh")
+ .args(["-c", "vtysh -c 'show ip route ospf json'"])
+ .output()?
+ .stdout,
+ )?;
+ let ospf_routes: proxmox_frr::de::Routes = if ospf_routes_string.is_empty() {
+ proxmox_frr::de::Routes::default()
+ } else {
+ serde_json::from_str(&ospf_routes_string)
+ .with_context(|| "error parsing ospf routes")?
+ };
+
+ status::get_routes(fabric_id, config, ospf_routes)
+ }
+ }
+ }
+
/// Return the status of all fabrics on this node.
///
/// Go through all fabrics in the config, then filter out the ones that exist on this node.
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index 0c9dc0f..ba7fcf7 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -10,10 +10,19 @@ use proxmox_ve_config::{
common::valid::Valid,
sdn::fabric::{
FabricConfig,
- section_config::{Section, fabric::FabricId, node::Node as ConfigNode},
+ section_config::{Section, fabric::FabricId, node::Node as ConfigNode, node::NodeId},
},
};
+/// The status of a route.
+///
+/// Contains the route and all the nexthops. This is common across all protocols.
+#[derive(Debug, Serialize)]
+pub struct RouteStatus {
+ route: String,
+ via: Vec<String>,
+}
+
/// Protocol
#[derive(Debug, Serialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
@@ -73,6 +82,89 @@ pub struct FabricsRunningConfig {
pub ids: BTreeMap<String, Section>,
}
+/// Converts the parsed `show ip route x` frr route output into a list of common [`RouteStatus`]
+/// structs.
+///
+/// We always execute `show ip route <protocol>` so we only get routes generated from a specific
+/// protocol. The problem is that we can't definitely link a specific route to a specific fabric.
+/// To solve this, we retrieve all the interfaces configured on a fabric on this node and check
+/// which route contains a output interface of the fabric.
+pub fn get_routes(
+ fabric_id: FabricId,
+ config: Valid<FabricConfig>,
+ routes: de::Routes,
+) -> Result<Vec<RouteStatus>, anyhow::Error> {
+ let hostname = proxmox_sys::nodename();
+
+ let mut stats: Vec<RouteStatus> = Vec::new();
+
+ if let Ok(node) = config
+ .get_fabric(&fabric_id)?
+ .get_node(&NodeId::from_string(hostname.to_string())?)
+ {
+ let mut interface_names: HashSet<&str> = match node {
+ ConfigNode::Openfabric(n) => n
+ .properties()
+ .interfaces()
+ .map(|i| i.name().as_str())
+ .collect(),
+ ConfigNode::Ospf(n) => n
+ .properties()
+ .interfaces()
+ .map(|i| i.name().as_str())
+ .collect(),
+ };
+
+ let dummy_interface = format!("dummy_{}", fabric_id.as_str());
+ interface_names.insert(&dummy_interface);
+
+ for (route_key, route_list) in routes.0 {
+ let mut route_belongs_to_fabric = false;
+ for route in &route_list {
+ if !route.installed.unwrap_or_default() {
+ continue;
+ }
+
+ for nexthop in &route.nexthops {
+ if let Some(iface_name) = &nexthop.interface_name {
+ if interface_names.contains(iface_name.as_str()) {
+ route_belongs_to_fabric = true;
+ break;
+ }
+ }
+ }
+ if route_belongs_to_fabric {
+ break;
+ }
+ }
+
+ if route_belongs_to_fabric {
+ let mut via_list = Vec::new();
+ for route in route_list {
+ for nexthop in &route.nexthops {
+ let via = if let Some(ip) = nexthop.ip {
+ ip.to_string()
+ } else if let Some(iface_name) = &nexthop.interface_name {
+ iface_name.clone()
+ } else if let Some(true) = &nexthop.unreachable {
+ "unreachable".to_string()
+ } else {
+ continue;
+ };
+ via_list.push(via);
+ }
+ }
+
+ stats.push(RouteStatus {
+ route: route_key.to_string(),
+ via: via_list,
+ });
+ }
+ }
+ }
+ Ok(stats)
+}
+
/// Get the status for each fabric using the parsed routes from frr
///
/// Using the parsed routes we get from frr, filter and map them to a HashMap mapping every
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs 09/10] pve-rs: fabrics: add function to get the interfaces used for a fabric
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (15 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 08/10] pve-rs: fabrics: add function to get routes learned by a fabric Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 10/10] pve-rs: fabrics: add function to get the neighbors " Stefan Hanreich
` (17 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add a function which queries FRR to return all the interfaces which are
used on the local node by a specific fabric. Again, we can associate a
fabric with a specific openfabric/ospf interface by reading and parsing
the fabric config and getting all the interfaces configured on the node
and matching them to the FRR output.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 50 +++++++++++
pve-rs/src/sdn/status.rs | 133 ++++++++++++++++++++++++++++-
2 files changed, 182 insertions(+), 1 deletion(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 5fbb67e..150b7fa 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -666,6 +666,56 @@ pub mod pve_rs_sdn_fabrics {
}
}
+ /// Get the interfaces for this specific fabric on this node
+ ///
+ /// Read and parse the fabric config to get the protocol of the fabric and retrieve the
+ /// interfaces (ospf). Convert the frr output into a common format of fabric interfaces.
+ #[export]
+ fn interfaces(fabric_id: FabricId) -> Result<status::InterfaceStatus, Error> {
+ // Read fabric config to get protocol of fabric
+ let config = get_fabrics_config()?;
+
+ let fabric = config.get_fabric(&fabric_id)?;
+
+ match fabric {
+ FabricEntry::Openfabric(_) => {
+ let openfabric_interface_string = String::from_utf8(
+ Command::new("sh")
+ .args(["-c", "vtysh -c 'show openfabric interface json'"])
+ .output()?
+ .stdout,
+ )?;
+ let openfabric_interfaces: proxmox_frr::de::openfabric::Interfaces =
+ if openfabric_interface_string.is_empty() {
+ proxmox_frr::de::openfabric::Interfaces::default()
+ } else {
+ serde_json::from_str(&openfabric_interface_string)
+ .with_context(|| "error parsing openfabric interfaces")?
+ };
+
+ status::get_interfaces_openfabric(fabric_id, openfabric_interfaces)
+ .map(|v| v.into())
+ }
+ FabricEntry::Ospf(fabric) => {
+ let ospf_interfaces_string = String::from_utf8(
+ Command::new("sh")
+ .args(["-c", "vtysh -c 'show ip ospf interface json'"])
+ .output()?
+ .stdout,
+ )?;
+ let ospf_interfaces: proxmox_frr::de::ospf::Interfaces =
+ if ospf_interfaces_string.is_empty() {
+ proxmox_frr::de::ospf::Interfaces::default()
+ } else {
+ serde_json::from_str(&ospf_interfaces_string)
+ .with_context(|| "error parsing ospf interfaces")?
+ };
+
+ status::get_interfaces_ospf(fabric_id, fabric, ospf_interfaces).map(|v| v.into())
+ }
+ }
+ }
+
/// Return the status of all fabrics on this node.
///
/// Go through all fabrics in the config, then filter out the ones that exist on this node.
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index ba7fcf7..450bb6c 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -6,14 +6,79 @@ use proxmox_network_types::mac_address::MacAddress;
use serde::{Deserialize, Serialize};
use proxmox_frr::de::{self};
+use proxmox_ve_config::sdn::fabric::section_config::protocol::ospf::{
+ OspfNodeProperties, OspfProperties,
+};
use proxmox_ve_config::{
common::valid::Valid,
sdn::fabric::{
- FabricConfig,
+ Entry, FabricConfig,
section_config::{Section, fabric::FabricId, node::Node as ConfigNode, node::NodeId},
},
};
+// The status of a fabric interface
+//
+// Either up or down.
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum InterfaceState {
+ Up,
+ Down,
+}
+
+mod ospf {
+ use proxmox_frr::de;
+ use serde::Serialize;
+
+ /// 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)]
+ pub struct InterfaceStatus {
+ pub name: String,
+ pub state: super::InterfaceState,
+ #[serde(rename = "type")]
+ pub ty: de::ospf::NetworkType,
+ }
+}
+mod openfabric {
+ use proxmox_frr::de;
+ use serde::Serialize;
+
+ /// 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)]
+ pub struct InterfaceStatus {
+ pub name: String,
+ pub state: de::openfabric::CircuitState,
+ #[serde(rename = "type")]
+ pub ty: de::openfabric::NetworkType,
+ }
+}
+
+/// Common InterfaceStatus that contains either OSPF or Openfabric interfaces
+#[derive(Debug, Serialize)]
+#[serde(untagged)]
+pub enum InterfaceStatus {
+ Openfabric(Vec<openfabric::InterfaceStatus>),
+ Ospf(Vec<ospf::InterfaceStatus>),
+}
+
+impl From<Vec<openfabric::InterfaceStatus>> for InterfaceStatus {
+ fn from(value: Vec<openfabric::InterfaceStatus>) -> Self {
+ InterfaceStatus::Openfabric(value)
+ }
+}
+impl From<Vec<ospf::InterfaceStatus>> for InterfaceStatus {
+ fn from(value: Vec<ospf::InterfaceStatus>) -> Self {
+ InterfaceStatus::Ospf(value)
+ }
+}
+
/// The status of a route.
///
/// Contains the route and all the nexthops. This is common across all protocols.
@@ -165,6 +230,72 @@ pub fn get_routes(
Ok(stats)
}
+/// Conver the `show openfabric interface` output into a list of [`openfabric::InterfaceStatus`].
+///
+/// Openfabric uses the name of the fabric as an "area", so simply match that to the fabric_id.
+pub fn get_interfaces_openfabric(
+ fabric_id: FabricId,
+ interfaces: de::openfabric::Interfaces,
+) -> Result<Vec<openfabric::InterfaceStatus>, anyhow::Error> {
+ let mut stats: Vec<openfabric::InterfaceStatus> = Vec::new();
+
+ for area in &interfaces.areas {
+ if area.area == fabric_id.as_str() {
+ for circuit in &area.circuits {
+ stats.push(openfabric::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
+/// the fabric on this specific node and then match the interfaces to the fabric using the
+/// interface names.
+pub fn get_interfaces_ospf(
+ fabric_id: FabricId,
+ fabric: &Entry<OspfProperties, OspfNodeProperties>,
+ neighbors: de::ospf::Interfaces,
+) -> Result<Vec<ospf::InterfaceStatus>, anyhow::Error> {
+ let hostname = proxmox_sys::nodename();
+
+ let mut stats: Vec<ospf::InterfaceStatus> = Vec::new();
+
+ if let Ok(node) = fabric.node_section(&NodeId::from_string(hostname.to_string())?) {
+ let mut fabric_interface_names: HashSet<&str> = node
+ .properties()
+ .interfaces()
+ .map(|i| i.name().as_str())
+ .collect();
+
+ let dummy_interface = format!("dummy_{}", fabric_id.as_str());
+ fabric_interface_names.insert(&dummy_interface);
+
+ for (interface_name, interface) in &neighbors.interfaces {
+ if fabric_interface_names.contains(interface_name.as_str()) {
+ stats.push(ospf::InterfaceStatus {
+ name: interface_name.to_string(),
+ state: if interface.if_up {
+ InterfaceState::Up
+ } else {
+ InterfaceState::Down
+ },
+ ty: interface.network_type,
+ });
+ }
+ }
+ }
+
+ Ok(stats)
+}
+
/// Get the status for each fabric using the parsed routes from frr
///
/// Using the parsed routes we get from frr, filter and map them to a HashMap mapping every
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs 10/10] pve-rs: fabrics: add function to get the neighbors for a fabric
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (16 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 09/10] pve-rs: fabrics: add function to get the interfaces used for " Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 1/9] refactor: rework api module structure for the /nodes/{node}/sdn subdir Stefan Hanreich
` (16 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add a function that takes a fabricId and returns all routing protocol
neighbors (OpenFabric or OSPF) identified by FRR. For OpenFabric, the
fabric name is used as the area identifier, so we can filter easily. For
OSPF, the fabric configuration is read/parsed to extract interfaces,
which are then matched against the FRR neighbor output. The fabric
configuration is always read first to verify that a fabric exists before
querying FRR, as configuration parsing is (probably) faster than vtysh
execution.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 49 +++++++++++
pve-rs/src/sdn/status.rs | 131 +++++++++++++++++++++++++++++
2 files changed, 180 insertions(+)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 150b7fa..5230aa3 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -666,6 +666,55 @@ pub mod pve_rs_sdn_fabrics {
}
}
+ /// Get the neighbors for this specific fabric on this node
+ ///
+ /// Read and parse the fabric config to get the fabric protocol and the interfaces (ospf).
+ /// Parse the frr output of the neighbor commands and return a common format.
+ #[export]
+ fn neighbors(fabric_id: FabricId) -> Result<status::NeighborStatus, Error> {
+ // Read fabric config to get protocol of fabric
+ let config = get_fabrics_config()?;
+
+ let fabric = config.get_fabric(&fabric_id)?;
+
+ match fabric {
+ FabricEntry::Openfabric(_) => {
+ let openfabric_neighbors_string = String::from_utf8(
+ Command::new("sh")
+ .args(["-c", "vtysh -c 'show openfabric neighbor detail json'"])
+ .output()?
+ .stdout,
+ )?;
+ let openfabric_neighbors: proxmox_frr::de::openfabric::Neighbors =
+ if openfabric_neighbors_string.is_empty() {
+ proxmox_frr::de::openfabric::Neighbors::default()
+ } else {
+ serde_json::from_str(&openfabric_neighbors_string)
+ .with_context(|| "error parsing openfabric neighbors")?
+ };
+
+ status::get_neighbors_openfabric(fabric_id, openfabric_neighbors).map(|v| v.into())
+ }
+ FabricEntry::Ospf(fabric) => {
+ let ospf_neighbors_string = String::from_utf8(
+ Command::new("sh")
+ .args(["-c", "vtysh -c 'show ip ospf neighbor json'"])
+ .output()?
+ .stdout,
+ )?;
+ let ospf_neighbors: proxmox_frr::de::ospf::Neighbors =
+ if ospf_neighbors_string.is_empty() {
+ proxmox_frr::de::ospf::Neighbors::default()
+ } else {
+ serde_json::from_str(&ospf_neighbors_string)
+ .with_context(|| "error parsing ospf neighbors")?
+ };
+
+ status::get_neighbors_ospf(fabric_id, fabric, ospf_neighbors).map(|v| v.into())
+ }
+ }
+ }
+
/// Get the interfaces for this specific fabric on this node
///
/// Read and parse the fabric config to get the protocol of the fabric and retrieve the
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index 450bb6c..881fb2d 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -31,6 +31,16 @@ mod ospf {
use proxmox_frr::de;
use serde::Serialize;
+ /// The status of a neighbor.
+ ///
+ /// Contains the neighbor name and the neighbor status.
+ #[derive(Debug, Serialize)]
+ pub struct NeighborStatus {
+ pub neighbor: String,
+ pub status: String,
+ 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
@@ -47,6 +57,16 @@ mod openfabric {
use proxmox_frr::de;
use serde::Serialize;
+ /// The status of a neighbor.
+ ///
+ /// Contains the neighbor name and the neighbor status.
+ #[derive(Debug, Serialize)]
+ pub struct NeighborStatus {
+ pub neighbor: String,
+ pub status: de::openfabric::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
@@ -60,6 +80,25 @@ mod openfabric {
}
}
+/// Common NeighborStatus that contains either OSPF or Openfabric neighbors
+#[derive(Debug, Serialize)]
+#[serde(untagged)]
+pub enum NeighborStatus {
+ Openfabric(Vec<openfabric::NeighborStatus>),
+ Ospf(Vec<ospf::NeighborStatus>),
+}
+
+impl From<Vec<openfabric::NeighborStatus>> for NeighborStatus {
+ fn from(value: Vec<openfabric::NeighborStatus>) -> Self {
+ NeighborStatus::Openfabric(value)
+ }
+}
+impl From<Vec<ospf::NeighborStatus>> for NeighborStatus {
+ fn from(value: Vec<ospf::NeighborStatus>) -> Self {
+ NeighborStatus::Ospf(value)
+ }
+}
+
/// Common InterfaceStatus that contains either OSPF or Openfabric interfaces
#[derive(Debug, Serialize)]
#[serde(untagged)]
@@ -230,6 +269,98 @@ pub fn get_routes(
Ok(stats)
}
+/// Convert the parsed openfabric neighbor neighbor information into a list of
+/// [`openfabric::NeighborStatus`].
+///
+/// OpenFabric uses the name of the fabric as an "area", so simply match that to the fabric_id.
+pub fn get_neighbors_openfabric(
+ fabric_id: FabricId,
+ neighbors: de::openfabric::Neighbors,
+) -> Result<Vec<openfabric::NeighborStatus>, anyhow::Error> {
+ let mut stats: Vec<openfabric::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(openfabric::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
+/// the fabric on this specific node and then match the neighbors to the fabric using the
+/// interfaces.
+pub fn get_neighbors_ospf(
+ fabric_id: FabricId,
+ fabric: &Entry<OspfProperties, OspfNodeProperties>,
+ neighbors: de::ospf::Neighbors,
+) -> Result<Vec<ospf::NeighborStatus>, anyhow::Error> {
+ let hostname = proxmox_sys::nodename();
+
+ let mut stats: Vec<ospf::NeighborStatus> = Vec::new();
+
+ if let Ok(node) = fabric.node_section(&NodeId::from_string(hostname.to_string())?) {
+ let mut interface_names: HashSet<&str> = node
+ .properties()
+ .interfaces()
+ .map(|i| i.name().as_str())
+ .collect();
+
+ let dummy_interface = format!("dummy_{}", fabric_id.as_str());
+ interface_names.insert(&dummy_interface);
+
+ for (neighbor_key, neighbor_list) in &neighbors.neighbors {
+ let mut has_matching_neighbor = false;
+ for neighbor in neighbor_list {
+ match neighbor.interface_name.split_once(":") {
+ Some((interface_name, _)) => {
+ if interface_names.contains(interface_name) {
+ has_matching_neighbor = true;
+ break;
+ }
+ }
+ _ => {
+ continue;
+ }
+ }
+ }
+ if has_matching_neighbor {
+ let status = neighbor_list
+ .first()
+ .map(|n| n.neighbor_state.clone())
+ .unwrap_or_default();
+ let uptime = neighbor_list
+ .first()
+ .map(|n| n.up_time.clone())
+ .unwrap_or_default();
+ stats.push(ospf::NeighborStatus {
+ neighbor: neighbor_key.clone(),
+ status,
+ uptime,
+ });
+ }
+ }
+ }
+
+ Ok(stats)
+}
+
/// Conver the `show openfabric interface` output into a list of [`openfabric::InterfaceStatus`].
///
/// Openfabric uses the name of the fabric as an "area", so simply match that to the fabric_id.
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-network 1/9] refactor: rework api module structure for the /nodes/{node}/sdn subdir
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (17 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 10/10] pve-rs: fabrics: add function to get the neighbors " Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 2/9] fabrics: add fabrics status to SDN::status function Stefan Hanreich
` (15 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
Previously, the endpoints for the SDN browser were contained in the
PVE::API2::Network::SDN::Zones module, which was registered in
PVE::API2::Nodes (located in pve-manager) directly with the sub-path
'sdn/{zone}'.
Instead of importing the zones module in pve-manager directly, create
a top-level Nodes module in pve-network, that is then imported in
pve-manager under the path 'sdn'. By importing just the root-level
module, changes can be made to the pve-network API without having to
update pve-manager. This allows for a better module structure on
pve-network side, because different entities can then be managed in
pve-network directly in the new top-level module. This is mainly in
preparation for adding new Vnets and Fabrics modules.
Also, the zones' content endpoint was contained in its own module. In
order to make it easier to add new endpoints to the 'sdn/zones/{zone}'
directory, move the contents endpoint into a new module that is
imported directly under 'sdn/zones/{zone}'. This allows easily adding
endpoints under the 'sdn/zones/{zone}' sub directory, without having
to create a separate module for each endpoint and registering it in
the Status module.
No functional changes intended, every endpoint should be the same as
before (URL + returned information).
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Makefile | 2 +-
.../Network/SDN/{Zones => Nodes}/Makefile | 8 ++-
src/PVE/API2/Network/SDN/Nodes/Status.pm | 49 +++++++++++++++
.../SDN/{Zones/Content.pm => Nodes/Zone.pm} | 59 +++++++++++++++----
.../SDN/{Zones/Status.pm => Nodes/Zones.pm} | 56 +++---------------
5 files changed, 111 insertions(+), 63 deletions(-)
rename src/PVE/API2/Network/SDN/{Zones => Nodes}/Makefile (62%)
create mode 100644 src/PVE/API2/Network/SDN/Nodes/Status.pm
rename src/PVE/API2/Network/SDN/{Zones/Content.pm => Nodes/Zone.pm} (60%)
rename src/PVE/API2/Network/SDN/{Zones/Status.pm => Nodes/Zones.pm} (59%)
diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
index 08bec75..2624d9a 100644
--- a/src/PVE/API2/Network/SDN/Makefile
+++ b/src/PVE/API2/Network/SDN/Makefile
@@ -6,6 +6,6 @@ PERL5DIR=${DESTDIR}/usr/share/perl5
.PHONY: install
install:
for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/$$i; done
- make -C Zones install
make -C Fabrics install
+ make -C Nodes install
diff --git a/src/PVE/API2/Network/SDN/Zones/Makefile b/src/PVE/API2/Network/SDN/Nodes/Makefile
similarity index 62%
rename from src/PVE/API2/Network/SDN/Zones/Makefile
rename to src/PVE/API2/Network/SDN/Nodes/Makefile
index 9b0a42b..edf0225 100644
--- a/src/PVE/API2/Network/SDN/Zones/Makefile
+++ b/src/PVE/API2/Network/SDN/Nodes/Makefile
@@ -1,8 +1,10 @@
-SOURCES=Status.pm Content.pm
-
+SOURCES=\
+ Status.pm\
+ Zone.pm\
+ Zones.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
.PHONY: install
install:
- for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/Zones/$$i; done
+ for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/Nodes/$$i; done
diff --git a/src/PVE/API2/Network/SDN/Nodes/Status.pm b/src/PVE/API2/Network/SDN/Nodes/Status.pm
new file mode 100644
index 0000000..e862d4a
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Nodes/Status.pm
@@ -0,0 +1,49 @@
+package PVE::API2::Network::SDN::Nodes::Status;
+
+use strict;
+use warnings;
+
+use PVE::API2::Network::SDN::Nodes::Zones;
+
+use PVE::JSONSchema qw(get_standard_option);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Nodes::Zones",
+ path => 'zones',
+});
+
+__PACKAGE__->register_method({
+ name => 'sdnindex',
+ path => '',
+ method => 'GET',
+ permissions => { user => 'all' },
+ description => "SDN index.",
+ proxyto => 'node',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {},
+ },
+ links => [{ rel => 'child', href => "{name}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $result = [
+ { name => 'zones' },
+ ];
+ return $result;
+ },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Zones/Content.pm b/src/PVE/API2/Network/SDN/Nodes/Zone.pm
similarity index 60%
rename from src/PVE/API2/Network/SDN/Zones/Content.pm
rename to src/PVE/API2/Network/SDN/Nodes/Zone.pm
index 7666321..1e963fc 100644
--- a/src/PVE/API2/Network/SDN/Zones/Content.pm
+++ b/src/PVE/API2/Network/SDN/Nodes/Zone.pm
@@ -1,26 +1,65 @@
-package PVE::API2::Network::SDN::Zones::Content;
+package PVE::API2::Network::SDN::Nodes::Zone;
use strict;
use warnings;
-use PVE::SafeSyslog;
-use PVE::Cluster;
-use PVE::INotify;
+use JSON qw(decode_json);
+
use PVE::Exception qw(raise_param_exc);
-use PVE::RPCEnvironment;
-use PVE::RESTHandler;
+use PVE::INotify;
+use PVE::IPRoute2;
use PVE::JSONSchema qw(get_standard_option);
-use PVE::Network::SDN;
+use PVE::Network;
+use PVE::Network::SDN::Vnets;
+use PVE::Network::SDN::Zones;
+use PVE::RS::SDN::Fabrics;
+use PVE::Tools qw(extract_param run_command);
+use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method({
- name => 'index',
+ name => 'diridx',
path => '',
method => 'GET',
+ description => "Directory index for SDN zone status.",
+ permissions => {
+ check => ['perm', '/sdn/zones/{zone}', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ zone => get_standard_option('pve-sdn-zone-id'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ subdir => { type => 'string' },
+ },
+ },
+ links => [{ rel => 'child', href => "{subdir}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+ my $res = [
+ { subdir => 'content' }, { subdir => 'bridges' }, { subdir => 'ip-vrf' },
+ ];
+
+ return $res;
+ },
+});
+
+__PACKAGE__->register_method({
+ path => 'content',
+ name => 'index',
+ method => 'GET',
description => "List zone content.",
permissions => {
- check => ['perm', '/sdn/zones/{zone}', ['SDN.Audit'], any => 1],
+ check => ['perm', '/sdn/zones/{zone}', ['SDN.Audit']],
},
protected => 1,
proxyto => 'node',
@@ -70,7 +109,7 @@ __PACKAGE__->register_method({
my $res = [];
- my ($zone_status, $vnet_status) = PVE::Network::SDN::status();
+ my ($zone_status, $vnet_status) = PVE::Network::SDN::Zones::status();
foreach my $id (keys %{$vnet_status}) {
if ($vnet_status->{$id}->{zone} eq $zoneid) {
diff --git a/src/PVE/API2/Network/SDN/Zones/Status.pm b/src/PVE/API2/Network/SDN/Nodes/Zones.pm
similarity index 59%
rename from src/PVE/API2/Network/SDN/Zones/Status.pm
rename to src/PVE/API2/Network/SDN/Nodes/Zones.pm
index 4957567..54c444f 100644
--- a/src/PVE/API2/Network/SDN/Zones/Status.pm
+++ b/src/PVE/API2/Network/SDN/Nodes/Zones.pm
@@ -1,24 +1,17 @@
-package PVE::API2::Network::SDN::Zones::Status;
+package PVE::API2::Network::SDN::Nodes::Zones;
-use strict;
-use warnings;
-
-use File::Path;
-use File::Basename;
-use PVE::Tools;
+use PVE::API2::Network::SDN::Nodes::Zone;
use PVE::INotify;
-use PVE::Cluster;
-use PVE::API2::Network::SDN::Zones::Content;
-use PVE::RESTHandler;
-use PVE::RPCEnvironment;
use PVE::JSONSchema qw(get_standard_option);
-use PVE::Exception qw(raise_param_exc);
+use PVE::Network::SDN;
+use PVE::RPCEnvironment;
+use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method({
- subclass => "PVE::API2::Network::SDN::Zones::Content",
- path => '{zone}/content',
+ subclass => "PVE::API2::Network::SDN::Nodes::Zone",
+ path => '{zone}',
});
__PACKAGE__->register_method({
@@ -75,39 +68,4 @@ __PACKAGE__->register_method({
},
});
-__PACKAGE__->register_method({
- name => 'diridx',
- path => '{zone}',
- method => 'GET',
- description => "",
- permissions => {
- check => ['perm', '/sdn/zones/{zone}', ['SDN.Audit'], any => 1],
- },
- parameters => {
- additionalProperties => 0,
- properties => {
- node => get_standard_option('pve-node'),
- zone => get_standard_option('pve-sdn-zone-id'),
- },
- },
- returns => {
- type => 'array',
- items => {
- type => "object",
- properties => {
- subdir => { type => 'string' },
- },
- },
- links => [{ rel => 'child', href => "{subdir}" }],
- },
- code => sub {
- my ($param) = @_;
- my $res = [
- { subdir => 'content' },
- ];
-
- return $res;
- },
-});
-
1;
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-network 2/9] fabrics: add fabrics status to SDN::status function
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (18 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 1/9] refactor: rework api module structure for the /nodes/{node}/sdn subdir Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 3/9] sdn: status: add zone type to sdn resource Stefan Hanreich
` (14 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
This makes it so fabrics can be included in the SDN panel in the Web
UI. To avoid having to calculate the fabrics status information on
endpoints that only require the zone status, move the pre-existing
call sites over to the zone-specific function.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Nodes/Zones.pm | 2 +-
src/PVE/Network/SDN.pm | 6 ++++--
src/test/debug/statuscheck.pl | 3 ++-
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/src/PVE/API2/Network/SDN/Nodes/Zones.pm b/src/PVE/API2/Network/SDN/Nodes/Zones.pm
index 54c444f..b92be6a 100644
--- a/src/PVE/API2/Network/SDN/Nodes/Zones.pm
+++ b/src/PVE/API2/Network/SDN/Nodes/Zones.pm
@@ -56,7 +56,7 @@ __PACKAGE__->register_method({
my $res = [];
- my ($zone_status, $vnet_status) = PVE::Network::SDN::status();
+ my ($zone_status, $vnet_status) = PVE::Network::SDN::Zones::status();
foreach my $id (sort keys %{$zone_status}) {
my $item->{zone} = $id;
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index f2ecd4a..81374b0 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -16,6 +16,8 @@ use PVE::RESTEnvironment qw(log_warn);
use PVE::RPCEnvironment;
use PVE::Tools qw(file_get_contents file_set_contents extract_param dir_glob_regex run_command);
+use PVE::RS::SDN::Fabrics;
+
use PVE::Network::SDN::Vnets;
use PVE::Network::SDN::Zones;
use PVE::Network::SDN::Controllers;
@@ -97,9 +99,9 @@ sub ifquery_check {
}
sub status {
-
my ($zone_status, $vnet_status) = PVE::Network::SDN::Zones::status();
- return ($zone_status, $vnet_status);
+ my $fabric_status = PVE::RS::SDN::Fabrics::status();
+ return ($zone_status, $vnet_status, $fabric_status);
}
sub running_config {
diff --git a/src/test/debug/statuscheck.pl b/src/test/debug/statuscheck.pl
index e43003b..e963117 100644
--- a/src/test/debug/statuscheck.pl
+++ b/src/test/debug/statuscheck.pl
@@ -3,7 +3,8 @@ use warnings;
use PVE::Network::SDN;
use Data::Dumper;
-my ($transport_status, $vnet_status) = PVE::Network::SDN::status();
+my ($transport_status, $vnet_status, $fabric_status) = PVE::Network::SDN::status();
+print Dumper($fabric_status);
print Dumper($vnet_status);
print Dumper($transport_status);
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-network 3/9] sdn: status: add zone type to sdn resource
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (19 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 2/9] fabrics: add fabrics status to SDN::status function Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 4/9] api: nodes: fabrics: add endpoint for querying route status Stefan Hanreich
` (13 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
This will mainly be used by the UI determine whether to show panels
that are specific to a certain zone type (e.g. IP-VRF panel for EVPN
zones).
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Zones.pm | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/PVE/Network/SDN/Zones.pm b/src/PVE/Network/SDN/Zones.pm
index 5f30df3..050b88c 100644
--- a/src/PVE/Network/SDN/Zones.pm
+++ b/src/PVE/Network/SDN/Zones.pm
@@ -254,7 +254,9 @@ sub status {
next
if defined($zone_cfg->{ids}->{$id}->{nodes})
&& !$zone_cfg->{ids}->{$id}->{nodes}->{$nodename};
+
$zone_status->{$id}->{status} = $err_config ? 'pending' : 'available';
+ $zone_status->{$id}->{zone_type} = $zone_cfg->{ids}->{$id}->{type};
}
foreach my $id (sort keys %{ $vnet_cfg->{ids} }) {
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-network 4/9] api: nodes: fabrics: add endpoint for querying route status
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (20 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 3/9] sdn: status: add zone type to sdn resource Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 5/9] api: nodes: fabrics: add endpoint for querying neighbor information Stefan Hanreich
` (12 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
This endpoint returns all routes from a given fabric, that are
imported in to the kernel routing table. For more information about
the return value, consult the respective proxmox-perl-rs commit. It is
used by the NetworkBrowser panel in pve-manager.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Nodes/Fabric.pm | 94 +++++++++++++++++++++++
src/PVE/API2/Network/SDN/Nodes/Fabrics.pm | 16 ++++
src/PVE/API2/Network/SDN/Nodes/Makefile | 2 +
src/PVE/API2/Network/SDN/Nodes/Status.pm | 8 +-
4 files changed, 119 insertions(+), 1 deletion(-)
create mode 100644 src/PVE/API2/Network/SDN/Nodes/Fabric.pm
create mode 100644 src/PVE/API2/Network/SDN/Nodes/Fabrics.pm
diff --git a/src/PVE/API2/Network/SDN/Nodes/Fabric.pm b/src/PVE/API2/Network/SDN/Nodes/Fabric.pm
new file mode 100644
index 0000000..c1886b7
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Nodes/Fabric.pm
@@ -0,0 +1,94 @@
+package PVE::API2::Network::SDN::Nodes::Fabric;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Network::SDN::Fabrics;
+use PVE::RPCEnvironment;
+use PVE::RS::SDN::Fabrics;
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'diridx',
+ path => '',
+ method => 'GET',
+ description => "Directory index for SDN fabric status.",
+ permissions => {
+ check => ['perm', '/sdn/fabrics/{fabric}', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ fabric => get_standard_option('pve-sdn-fabric-id'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ subdir => { type => 'string' },
+ },
+ },
+ links => [{ rel => 'child', href => "{subdir}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+ my $res = [
+ { subdir => 'routes' },
+ ];
+
+ return $res;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'routes',
+ path => 'routes',
+ method => 'GET',
+ description => "Get routes of all fabrics.",
+ permissions => {
+ check => ['perm', '/sdn/fabrics/{fabric}', ['SDN.Audit']],
+ },
+ protected => 1,
+ proxyto => 'node',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ fabric => get_standard_option('pve-sdn-fabric-id'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ route => {
+ description => "The CIDR block for this routing table entry.",
+ type => 'string',
+ },
+ via => {
+ description => "A list of nexthops for that route.",
+ type => 'array',
+ items => {
+ type => 'string',
+ description => 'The IP address of the nexthop.',
+ },
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $fabric_id = extract_param($param, 'fabric');
+ return PVE::RS::SDN::Fabrics::routes($fabric_id);
+ },
+});
+
diff --git a/src/PVE/API2/Network/SDN/Nodes/Fabrics.pm b/src/PVE/API2/Network/SDN/Nodes/Fabrics.pm
new file mode 100644
index 0000000..bbf557b
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Nodes/Fabrics.pm
@@ -0,0 +1,16 @@
+package PVE::API2::Network::SDN::Nodes::Fabrics;
+
+use strict;
+use warnings;
+
+use PVE::API2::Network::SDN::Nodes::Fabric;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Nodes::Fabric",
+ path => '{fabric}',
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Nodes/Makefile b/src/PVE/API2/Network/SDN/Nodes/Makefile
index edf0225..4e4791a 100644
--- a/src/PVE/API2/Network/SDN/Nodes/Makefile
+++ b/src/PVE/API2/Network/SDN/Nodes/Makefile
@@ -1,4 +1,6 @@
SOURCES=\
+ Fabric.pm\
+ Fabrics.pm\
Status.pm\
Zone.pm\
Zones.pm
diff --git a/src/PVE/API2/Network/SDN/Nodes/Status.pm b/src/PVE/API2/Network/SDN/Nodes/Status.pm
index e862d4a..2ce2702 100644
--- a/src/PVE/API2/Network/SDN/Nodes/Status.pm
+++ b/src/PVE/API2/Network/SDN/Nodes/Status.pm
@@ -3,6 +3,7 @@ package PVE::API2::Network::SDN::Nodes::Status;
use strict;
use warnings;
+use PVE::API2::Network::SDN::Nodes::Fabrics;
use PVE::API2::Network::SDN::Nodes::Zones;
use PVE::JSONSchema qw(get_standard_option);
@@ -10,6 +11,11 @@ use PVE::JSONSchema qw(get_standard_option);
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Nodes::Fabrics",
+ path => 'fabrics',
+});
+
__PACKAGE__->register_method({
subclass => "PVE::API2::Network::SDN::Nodes::Zones",
path => 'zones',
@@ -40,7 +46,7 @@ __PACKAGE__->register_method({
my ($param) = @_;
my $result = [
- { name => 'zones' },
+ { name => 'fabrics' }, { name => 'zones' },
];
return $result;
},
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-network 5/9] api: nodes: fabrics: add endpoint for querying neighbor information
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (21 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 4/9] api: nodes: fabrics: add endpoint for querying route status Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 6/9] api: nodes: fabrics: add endpoint for querying interface status Stefan Hanreich
` (11 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
This endpoint returns all neighbors for a given fabric and their
current status. For more information about the return value, consult
the respective proxmox-perl-rs commit. It is used by the
NetworkBrowser panel.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Nodes/Fabric.pm | 48 +++++++++++++++++++++++-
1 file changed, 47 insertions(+), 1 deletion(-)
diff --git a/src/PVE/API2/Network/SDN/Nodes/Fabric.pm b/src/PVE/API2/Network/SDN/Nodes/Fabric.pm
index c1886b7..b5971c7 100644
--- a/src/PVE/API2/Network/SDN/Nodes/Fabric.pm
+++ b/src/PVE/API2/Network/SDN/Nodes/Fabric.pm
@@ -40,7 +40,7 @@ __PACKAGE__->register_method({
code => sub {
my ($param) = @_;
my $res = [
- { subdir => 'routes' },
+ { subdir => 'neighbors' }, { subdir => 'routes' },
];
return $res;
@@ -92,3 +92,49 @@ __PACKAGE__->register_method({
},
});
+__PACKAGE__->register_method({
+ name => 'neighbors',
+ path => 'neighbors',
+ method => 'GET',
+ description => "Get neighbors of all fabrics.",
+ permissions => {
+ check => ['perm', '/sdn/fabrics/{fabric}', ['SDN.Audit']],
+ },
+ protected => 1,
+ proxyto => 'node',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ fabric => get_standard_option('pve-sdn-fabric-id'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ neighbor => {
+ description => "The IP of the neighbor.",
+ type => 'string',
+ },
+ status => {
+ description => "The status of the neighbor, as returned by FRR.",
+ type => 'string',
+ },
+ uptime => {
+ description =>
+ "The uptime of this neighbor, as returned by FRR (e.g. 8h24m12s).",
+ type => 'string',
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $fabric_id = extract_param($param, 'fabric');
+ return PVE::RS::SDN::Fabrics::neighbors($fabric_id);
+ },
+});
+
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-network 6/9] api: nodes: fabrics: add endpoint for querying interface status
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (22 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 5/9] api: nodes: fabrics: add endpoint for querying neighbor information Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 7/9] api: nodes: zones: add bridge status Stefan Hanreich
` (10 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
This endpoint returns the state of interfaces that are part of the
fabric. For more information on the returned values, consult the
proxmox-perl-rs commit. It is used by the NetworkBrowser panel.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Nodes/Fabric.pm | 47 ++++++++++++++++++++++++
1 file changed, 47 insertions(+)
diff --git a/src/PVE/API2/Network/SDN/Nodes/Fabric.pm b/src/PVE/API2/Network/SDN/Nodes/Fabric.pm
index b5971c7..90ea5e4 100644
--- a/src/PVE/API2/Network/SDN/Nodes/Fabric.pm
+++ b/src/PVE/API2/Network/SDN/Nodes/Fabric.pm
@@ -138,3 +138,50 @@ __PACKAGE__->register_method({
},
});
+__PACKAGE__->register_method({
+ name => 'interfaces',
+ path => 'interfaces',
+ method => 'GET',
+ description => "Get neighbors of all fabrics.",
+ protected => 1,
+ permissions => {
+ check => ['perm', '/sdn/fabrics/{fabric}', ['SDN.Audit']],
+ },
+ proxyto => 'node',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ fabric => get_standard_option('pve-sdn-fabric-id'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ name => {
+ description => "The name of the network interface.",
+ type => 'string',
+ },
+ type => {
+ description =>
+ "The type of this interface in the fabric (e.g. Point-to-Point, Broadcast, ..).",
+ type => 'string',
+ },
+ state => {
+ description => "The current state of the interface.",
+ type => 'string',
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $fabric_id = extract_param($param, 'fabric');
+ return PVE::RS::SDN::Fabrics::interfaces($fabric_id);
+ },
+});
+
+1;
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-network 7/9] api: nodes: zones: add bridge status
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (23 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 6/9] api: nodes: fabrics: add endpoint for querying interface status Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 8/9] api: nodes: zones: add ip vrf endpoint for evpn zones Stefan Hanreich
` (9 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
This endpoint returns an overview of all ports that are currently
members of a bridge (the running state). Additionally it provides
information about the configured VLANs on VLAN-aware bridges. If the
special zone name 'localnetwork' is used, then this endpoint returns
the information for all bridges that are configured outside of SDN via
/etc/network/interfaces.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Nodes/Zone.pm | 173 +++++++++++++++++++++++++
1 file changed, 173 insertions(+)
diff --git a/src/PVE/API2/Network/SDN/Nodes/Zone.pm b/src/PVE/API2/Network/SDN/Nodes/Zone.pm
index 1e963fc..d7312df 100644
--- a/src/PVE/API2/Network/SDN/Nodes/Zone.pm
+++ b/src/PVE/API2/Network/SDN/Nodes/Zone.pm
@@ -124,4 +124,177 @@ __PACKAGE__->register_method({
},
});
+__PACKAGE__->register_method({
+ name => 'bridges',
+ path => 'bridges',
+ proxyto => 'node',
+ method => 'GET',
+ protected => 1,
+ description =>
+ "Get a list of all bridges (vnets) that are part of a zone, as well as the ports that are members of that bridge.",
+ permissions => {
+ check => ['perm', '/sdn/zones/{zone}', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ zone => {
+ type => 'string',
+ description => 'zone name or "localnetwork"',
+ },
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ description => 'List of bridges contained in the SDN zone.',
+ type => 'object',
+ properties => {
+ name => {
+ description => 'Name of the bridge.',
+ type => 'string',
+ },
+ vlan_filtering => {
+ description =>
+ 'Whether VLAN filtering is enabled for this bridge (= VLAN-aware).',
+ type => 'string',
+ },
+ ports => {
+ description => 'All ports that are members of the bridge',
+ type => 'array',
+ items => {
+ description => 'Information about bridge ports.',
+ type => 'object',
+ properties => {
+ name => {
+ description => 'The name of the bridge port.',
+ type => 'string',
+ },
+ vmid => {
+ description =>
+ 'The ID of the guest that this interface belongs to.',
+ type => 'number',
+ optional => 1,
+ },
+ index => {
+ description =>
+ 'The index of the guests network device that this interface belongs to.',
+ type => 'number',
+ optional => 1,
+ },
+ primary_vlan => {
+ description =>
+ 'The primary VLAN configured for the port of this bridge (= PVID). Only for VLAN-aware bridges.',
+ type => 'number',
+ optional => 1,
+ },
+ vlans => {
+ description =>
+ 'A list of VLANs and VLAN ranges that are allowed for this bridge port in addition to the primary VLAN. Only for VLAN-aware bridges.',
+ type => 'array',
+ items => {
+ description =>
+ 'A single VLAN (123) or a VLAN range (234-435).',
+ type => 'string',
+ },
+ optional => 1,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $zone_id = extract_param($param, 'zone');
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+
+ my @bridges_in_zone;
+ if ($zone_id eq 'localnetwork') {
+ my $interface_config = PVE::INotify::read_file('interfaces', 1);
+ my $interfaces = $interface_config->{data}->{ifaces};
+
+ @bridges_in_zone =
+ grep { $interfaces->{$_}->{type} eq 'bridge' } keys $interfaces->%*;
+ } else {
+ my $zone = PVE::Network::SDN::Zones::get_zone($zone_id, 1);
+
+ raise_param_exc({
+ zone => "zone does not exist",
+ })
+ if !$zone;
+
+ my $vnet_cfg = PVE::Network::SDN::Vnets::config(1);
+ @bridges_in_zone =
+ grep { $vnet_cfg->{ids}->{$_}->{zone} eq $zone_id } keys $vnet_cfg->{ids}->%*;
+ }
+
+ my $ip_details = PVE::Network::ip_link_details();
+ my $vlan_information = PVE::IPRoute2::get_vlan_information();
+
+ my $result = {};
+ for my $bridge_name (@bridges_in_zone) {
+ next
+ if !$rpcenv->check_any(
+ $authuser,
+ "/sdn/zones/$zone_id/$bridge_name",
+ ['SDN.Audit', 'SDN.Allocate'],
+ 1,
+ );
+
+ my $ip_link = $ip_details->{$bridge_name};
+
+ $result->{$bridge_name} = {
+ name => $bridge_name,
+ vlan_filtering => $ip_link->{linkinfo}->{info_data}->{vlan_filtering},
+ ports => [],
+ };
+ }
+
+ for my $interface (values $ip_details->%*) {
+ if (PVE::IPRoute2::ip_link_is_bridge_member($interface)) {
+ my $master = $interface->{master};
+
+ # avoid potential TOCTOU by just skipping over the interface,
+ # if we didn't get the master from 'ip link'
+ next if !defined($result->{$master});
+
+ my $ifname = $interface->{ifname};
+
+ my $port = {
+ name => $ifname,
+ };
+
+ if ($ifname =~ m/^(?:fwpr(\d+)p(\d+)|veth(\d+)i(\d+)|tap(\d+)i(\d+))$/) {
+ $port->{vmid} = $1;
+ $port->{index} = $2;
+ }
+
+ if ($result->{$master}->{vlan_filtering} == 1) {
+ $port->{vlans} = [];
+
+ for my $vlan ($vlan_information->{$ifname}->{vlans}->@*) {
+ if (grep { $_ eq 'PVID' } $vlan->{flags}->@*) {
+ $port->{primary_vlan} = $vlan->{vlan};
+ } elsif ($vlan->{vlan} && $vlan->{vlanEnd}) {
+ push $port->{vlans}->@*, "$vlan->{vlan}-$vlan->{vlanEnd}";
+ } elsif ($vlan->{vlan}) {
+ push $port->{vlans}->@*, "$vlan->{vlan}";
+ }
+ }
+ }
+
+ push $result->{$master}->{ports}->@*, $port;
+ }
+ }
+
+ my @result = values $result->%*;
+ return \@result;
+ },
+});
+
1;
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-network 8/9] api: nodes: zones: add ip vrf endpoint for evpn zones
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (24 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 7/9] api: nodes: zones: add bridge status Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 9/9] api: nodes: vnets: add mac-vrf endpoint for evpn vnets Stefan Hanreich
` (8 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
This endpoint returns the state of the routing table on a node for a
given EVPN zone. This is used by the SDN browser panel to display
status information in the UI.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Nodes/Zone.pm | 79 ++++++++++++++++++++++++++
1 file changed, 79 insertions(+)
diff --git a/src/PVE/API2/Network/SDN/Nodes/Zone.pm b/src/PVE/API2/Network/SDN/Nodes/Zone.pm
index d7312df..43acbe9 100644
--- a/src/PVE/API2/Network/SDN/Nodes/Zone.pm
+++ b/src/PVE/API2/Network/SDN/Nodes/Zone.pm
@@ -297,4 +297,83 @@ __PACKAGE__->register_method({
},
});
+__PACKAGE__->register_method({
+ name => 'ip-vrf',
+ path => 'ip-vrf',
+ proxyto => 'node',
+ method => 'GET',
+ protected => 1,
+ description => "Get the IP VRF of an EVPN zone.",
+ permissions => {
+ check => ['perm', '/sdn/zones/{zone}', ['SDN.Audit']],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ zone => {
+ type => 'string',
+ description => 'Name of an EVPN zone.',
+ },
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ description => 'All entries in the VRF table of zone {zone} of the node.'
+ . 'This does not include /32 routes for guests on this host,'
+ . 'since they are handled via the respective vnet bridge directly.',
+ type => 'array',
+ items => {
+ type => 'object',
+ properties => {
+ ip => {
+ type => 'string',
+ format => 'CIDR',
+ description => 'The CIDR of the route table entry.',
+ },
+ metric => {
+ type => 'integer',
+ description => 'This route\'s metric.',
+ },
+ protocol => {
+ type => 'string',
+ description => 'The protocol where this route was learned from (e.g. BGP).',
+ },
+ 'nexthops' => {
+ type => 'array',
+ description => 'A list of nexthops for the route table entry.',
+ items => {
+ type => 'string',
+ description => 'the interface name or ip address of the next hop',
+ },
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $zone_id = extract_param($param, 'zone');
+ my $zone = PVE::Network::SDN::Zones::get_zone($zone_id, 1);
+
+ raise_param_exc({
+ zone => "zone does not exist",
+ })
+ if !$zone;
+
+ raise_param_exc({
+ zone => "zone is not an EVPN zone",
+ })
+ if $zone->{type} ne 'evpn';
+
+ my $node_id = extract_param($param, 'node');
+
+ raise_param_exc({
+ zone => "zone does not exist on node $node_id",
+ })
+ if defined($zone->{nodes}) && !grep { $_ eq $node_id } $zone->{nodes}->@*;
+
+ return PVE::RS::SDN::Fabrics::l3vpn_routes($zone_id);
+ },
+});
+
1;
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-network 9/9] api: nodes: vnets: add mac-vrf endpoint for evpn vnets
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (25 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 8/9] api: nodes: zones: add ip vrf endpoint for evpn zones Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 1/8] api: nodes: use new status module for sdn subdirectory Stefan Hanreich
` (7 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
This endpoint returns the current L2VNI of a given EVPN VNet, as
learned via BGP. This is used by the SDN browser to provide status
information for the EVPN vnet.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Nodes/Makefile | 2 +
src/PVE/API2/Network/SDN/Nodes/Status.pm | 8 +-
src/PVE/API2/Network/SDN/Nodes/Vnet.pm | 147 +++++++++++++++++++++++
src/PVE/API2/Network/SDN/Nodes/Vnets.pm | 16 +++
src/PVE/API2/Network/SDN/Vnets.pm | 2 +-
5 files changed, 173 insertions(+), 2 deletions(-)
create mode 100644 src/PVE/API2/Network/SDN/Nodes/Vnet.pm
create mode 100644 src/PVE/API2/Network/SDN/Nodes/Vnets.pm
diff --git a/src/PVE/API2/Network/SDN/Nodes/Makefile b/src/PVE/API2/Network/SDN/Nodes/Makefile
index 4e4791a..e70d2ce 100644
--- a/src/PVE/API2/Network/SDN/Nodes/Makefile
+++ b/src/PVE/API2/Network/SDN/Nodes/Makefile
@@ -2,6 +2,8 @@ SOURCES=\
Fabric.pm\
Fabrics.pm\
Status.pm\
+ Vnet.pm\
+ Vnets.pm\
Zone.pm\
Zones.pm
diff --git a/src/PVE/API2/Network/SDN/Nodes/Status.pm b/src/PVE/API2/Network/SDN/Nodes/Status.pm
index 2ce2702..7977e0c 100644
--- a/src/PVE/API2/Network/SDN/Nodes/Status.pm
+++ b/src/PVE/API2/Network/SDN/Nodes/Status.pm
@@ -5,6 +5,7 @@ use warnings;
use PVE::API2::Network::SDN::Nodes::Fabrics;
use PVE::API2::Network::SDN::Nodes::Zones;
+use PVE::API2::Network::SDN::Nodes::Vnets;
use PVE::JSONSchema qw(get_standard_option);
@@ -21,6 +22,11 @@ __PACKAGE__->register_method({
path => 'zones',
});
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Nodes::Vnets",
+ path => 'vnets',
+});
+
__PACKAGE__->register_method({
name => 'sdnindex',
path => '',
@@ -46,7 +52,7 @@ __PACKAGE__->register_method({
my ($param) = @_;
my $result = [
- { name => 'fabrics' }, { name => 'zones' },
+ { name => 'fabrics' }, { name => 'vnets' }, { name => 'zones' },
];
return $result;
},
diff --git a/src/PVE/API2/Network/SDN/Nodes/Vnet.pm b/src/PVE/API2/Network/SDN/Nodes/Vnet.pm
new file mode 100644
index 0000000..d5dae56
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Nodes/Vnet.pm
@@ -0,0 +1,147 @@
+package PVE::API2::Network::SDN::Nodes::Vnet;
+
+use strict;
+use warnings;
+
+use PVE::API2::Network::SDN::Vnets;
+use PVE::Exception qw(raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Network::SDN::Vnets;
+use PVE::Network::SDN::Zones;
+use PVE::RS::SDN::Fabrics;
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ name => 'diridx',
+ path => '',
+ method => 'GET',
+ description => "",
+ permissions => {
+ description => "Require 'SDN.Audit' permissions on '/sdn/zones/<zone>/<vnet>'",
+ user => 'all',
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ vnet => get_standard_option(
+ 'pve-sdn-vnet-id',
+ {
+ completion => \&PVE::Network::SDN::Vnets::complete_sdn_vnets,
+ },
+ ),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ subdir => { type => 'string' },
+ },
+ },
+ links => [{ rel => 'child', href => "{subdir}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $vnet_id = extract_param($param, 'vnet');
+ $PVE::API2::Network::SDN::Vnets::check_vnet_access->($vnet_id, ['SDN.Audit']);
+
+ my $res = [
+ { subdir => 'mac-vrf' },
+ ];
+
+ return $res;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'mac-vrf',
+ path => 'mac-vrf',
+ proxyto => 'node',
+ method => 'GET',
+ description => "Get the MAC VRF for a VNet in an EVPN zone.",
+ protected => 1,
+ permissions => {
+ description => "Require 'SDN.Audit' permissions on '/sdn/zones/<zone>/<vnet>'",
+ user => 'all',
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ vnet => get_standard_option(
+ 'pve-sdn-vnet-id',
+ {
+ completion => \&PVE::Network::SDN::Vnets::complete_sdn_vnets,
+ },
+ ),
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ description =>
+ 'All routes from the MAC VRF that this node self-originates or has learned via BGP.',
+ type => 'array',
+ items => {
+ type => 'object',
+ properties => {
+ ip => {
+ type => 'string',
+ format => 'ip',
+ description => 'The IP address of the MAC VRF entry.',
+ },
+ mac => {
+ type => 'string',
+ format => 'mac-addr',
+ description => 'The MAC address of the MAC VRF entry.',
+ },
+ 'nexthop' => {
+ type => 'string',
+ format => 'ip',
+ description => 'The IP address of the nexthop.',
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $vnet_id = extract_param($param, 'vnet');
+
+ $PVE::API2::Network::SDN::Vnets::check_vnet_access->($vnet_id, ['SDN.Audit']);
+
+ my $vnet = PVE::Network::SDN::Vnets::get_vnet($vnet_id, 1);
+
+ raise_param_exc({
+ vnet => "vnet does not exist",
+ })
+ if !$vnet;
+
+ my $zone = PVE::Network::SDN::Zones::get_zone($vnet->{zone}, 1);
+
+ raise_param_exc({
+ zone => "zone $vnet->{zone} does not exist",
+ })
+ if !$zone;
+
+ raise_param_exc({
+ zone => "zone $vnet->{zone} is not an EVPN zone.",
+ })
+ if $zone->{type} ne 'evpn';
+
+ my $node_id = extract_param($param, 'node');
+
+ raise_param_exc({
+ zone => "zone $vnet->{zone} of vnet $vnet_id does not exist on node $node_id",
+ })
+ if defined($zone->{nodes}) && !grep { $_ eq $node_id } $zone->{nodes}->@*;
+
+ return PVE::RS::SDN::Fabrics::l2vpn_routes($vnet_id);
+ },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Nodes/Vnets.pm b/src/PVE/API2/Network/SDN/Nodes/Vnets.pm
new file mode 100644
index 0000000..4f07201
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Nodes/Vnets.pm
@@ -0,0 +1,16 @@
+package PVE::API2::Network::SDN::Nodes::Vnets;
+
+use strict;
+use warnings;
+
+use PVE::API2::Network::SDN::Nodes::Vnet;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Nodes::Vnet",
+ path => '{vnet}',
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Vnets.pm b/src/PVE/API2/Network/SDN/Vnets.pm
index 1d9e500..b8faeac 100644
--- a/src/PVE/API2/Network/SDN/Vnets.pm
+++ b/src/PVE/API2/Network/SDN/Vnets.pm
@@ -63,7 +63,7 @@ my $api_sdn_vnets_deleted_config = sub {
}
};
-my $check_vnet_access = sub {
+our $check_vnet_access = sub {
my ($vnet, $privs) = @_;
my $cfg = PVE::Network::SDN::Vnets::config();
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-manager 1/8] api: nodes: use new status module for sdn subdirectory
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (26 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 9/9] api: nodes: vnets: add mac-vrf endpoint for evpn vnets Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 2/8] refactor: ui: sdn browser: parametrize zone content panel Stefan Hanreich
` (6 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
In order to avoid having to import every single new module for future
status reporting, SDN now exposes a single module that is imported
here.
Also remove the legacy have_sdn guards here, since SDN is
pre-installed for awhile and the guard therefore not needed anymore.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
PVE/API2/Nodes.pm | 50 ++++++-----------------------------------------
1 file changed, 6 insertions(+), 44 deletions(-)
diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm
index 4590b6186..08d9d1b51 100644
--- a/PVE/API2/Nodes.pm
+++ b/PVE/API2/Nodes.pm
@@ -49,6 +49,7 @@ use PVE::API2::Hardware;
use PVE::API2::LXC::Status;
use PVE::API2::LXC;
use PVE::API2::Network;
+use PVE::API2::Network::SDN::Nodes::Status;
use PVE::API2::NodeConfig;
use PVE::API2::Qemu::CPU;
use PVE::API2::Qemu;
@@ -60,12 +61,6 @@ use PVE::API2::Subscription;
use PVE::API2::Tasks;
use PVE::API2::VZDump;
-my $have_sdn;
-eval {
- require PVE::API2::Network::SDN::Zones::Status;
- $have_sdn = 1;
-};
-
use base qw(PVE::RESTHandler);
my $verify_command_item_desc = {
@@ -201,42 +196,10 @@ __PACKAGE__->register_method({
path => 'config',
});
-if ($have_sdn) {
- __PACKAGE__->register_method({
- subclass => "PVE::API2::Network::SDN::Zones::Status",
- path => 'sdn/zones',
- });
-
- __PACKAGE__->register_method({
- name => 'sdnindex',
- path => 'sdn',
- method => 'GET',
- permissions => { user => 'all' },
- description => "SDN index.",
- parameters => {
- additionalProperties => 0,
- properties => {
- node => get_standard_option('pve-node'),
- },
- },
- returns => {
- type => 'array',
- items => {
- type => "object",
- properties => {},
- },
- links => [{ rel => 'child', href => "{name}" }],
- },
- code => sub {
- my ($param) = @_;
-
- my $result = [
- { name => 'zones' },
- ];
- return $result;
- },
- });
-}
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Network::SDN::Nodes::Status",
+ path => 'sdn',
+});
__PACKAGE__->register_method({
name => 'index',
@@ -285,6 +248,7 @@ __PACKAGE__->register_method({
{ name => 'rrd' }, # fixme: remove?
{ name => 'rrddata' },
{ name => 'scan' },
+ { name => 'sdn' },
{ name => 'services' },
{ name => 'spiceshell' },
{ name => 'startall' },
@@ -303,8 +267,6 @@ __PACKAGE__->register_method({
{ name => 'wakeonlan' },
];
- push @$result, { name => 'sdn' } if $have_sdn;
-
return $result;
},
});
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-manager 2/8] refactor: ui: sdn browser: parametrize zone content panel
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (27 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 1/8] api: nodes: use new status module for sdn subdirectory Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 3/8] pvestatd: add network resource to status reporting Stefan Hanreich
` (5 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
In order to reuse the zone content panel for future, additional,
panels in the SDN browser, move all static configuration properties
into the declaration of the ExtJS widget. In particular, make the
on_(de)select callbacks configurable, so this panel can be customized
to work with a multitude of additional panels.
This is a preparation for extended status reporting for SDN zones,
which will add several new panels that want to utilize the content
view.
The selectionModel uses the default configuration that is included in
the GridPanel, so it can be removed.
No functional changes intended.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/sdn/ZoneContentPanel.js | 11 +++-
www/manager6/sdn/ZoneContentView.js | 75 +++++++++++++++-------------
2 files changed, 49 insertions(+), 37 deletions(-)
diff --git a/www/manager6/sdn/ZoneContentPanel.js b/www/manager6/sdn/ZoneContentPanel.js
index 76b8500b1..50d2dd741 100644
--- a/www/manager6/sdn/ZoneContentPanel.js
+++ b/www/manager6/sdn/ZoneContentPanel.js
@@ -18,12 +18,21 @@ Ext.define('PVE.sdn.ZoneContentPanel', {
var vnetview_panel = Ext.createWidget('pveSDNZoneContentView', {
title: 'VNets',
region: 'west',
- permissions_panel: permissions_panel,
+ sub_panel: permissions_panel,
nodename: me.nodename,
zone: me.zone,
width: '50%',
border: false,
split: true,
+
+ on_select: function (_sm, rec) {
+ let path = `/sdn/zones/${me.zone}/${rec.data.vnet}`;
+ permissions_panel.setPath(path);
+ },
+
+ on_deselect: function () {
+ permissions_panel.setPath(undefined);
+ },
});
Ext.apply(me, {
diff --git a/www/manager6/sdn/ZoneContentView.js b/www/manager6/sdn/ZoneContentView.js
index 97b3612e1..2243f277f 100644
--- a/www/manager6/sdn/ZoneContentView.js
+++ b/www/manager6/sdn/ZoneContentView.js
@@ -16,6 +16,43 @@ Ext.define(
groupHeaderTpl: '{name} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
},
],
+
+ sub_panel: null,
+
+ columns: [
+ {
+ header: gettext('VNet'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'vnet',
+ },
+ {
+ header: gettext('Alias'),
+ width: 300,
+ sortable: true,
+ dataIndex: 'alias',
+ },
+ {
+ header: gettext('Status'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'status',
+ },
+ {
+ header: gettext('Details'),
+ flex: 1,
+ dataIndex: 'statusmsg',
+ },
+ ],
+
+ on_select: function (selectionModel, record) {
+ // do nothing by default
+ },
+
+ on_deselect: function () {
+ // do nothing by default
+ },
+
initComponent: function () {
var me = this;
@@ -44,8 +81,6 @@ Ext.define(
},
});
- var sm = Ext.create('Ext.selection.RowModel', {});
-
var reload = function () {
store.load();
};
@@ -53,43 +88,11 @@ Ext.define(
Proxmox.Utils.monStoreErrors(me, store);
Ext.apply(me, {
store: store,
- selModel: sm,
- tbar: [],
- columns: [
- {
- header: 'VNet',
- width: 100,
- sortable: true,
- dataIndex: 'vnet',
- },
- {
- header: 'Alias',
- width: 300,
- sortable: true,
- dataIndex: 'alias',
- },
- {
- header: gettext('Status'),
- width: 100,
- sortable: true,
- dataIndex: 'status',
- },
- {
- header: gettext('Details'),
- flex: 1,
- dataIndex: 'statusmsg',
- },
- ],
listeners: {
activate: reload,
show: reload,
- select: function (_sm, rec) {
- let path = `/sdn/zones/${me.zone}/${rec.data.vnet}`;
- me.permissions_panel.setPath(path);
- },
- deselect: function () {
- me.permissions_panel.setPath(undefined);
- },
+ select: me.on_select,
+ deselect: me.on_deselect,
},
});
store.load();
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-manager 3/8] pvestatd: add network resource to status reporting
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (28 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 2/8] refactor: ui: sdn browser: parametrize zone content panel Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 4/8] pvestatd: sdn: adapt to changes in " Stefan Hanreich
` (4 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
The new network resource will act as the top-level resource for all
SDN / networking entities. The network resource contains a
network_type field, which indicates the type of networking resource -
similar to how the storage plugin handles different types of storages.
For now, it only contains SDN fabrics.
In the future, SDN zones, which are currently contained in the sdn
resource, will move to the network resource as well. To prepare for
this move, add support for the zone type in the API endpoint. To make
extending the resource with additional types easier, we ignore all
unknown network types in the API endpoint, so a node only returns the
types of network resources it can handle. This allows for easily
adding new types of network resources, without having to worry about
backwards-compatibility.
The main reason for moving over to a new resource type is the current
ID schema of the SDN resource, which is 'sdn/{zone_id}'. This makes it
hard to extend without the possibility of ID collisions. Additionally,
since the ID is used in several places throughout the backend / UI,
changing the schema would break compatibility with nodes that are on
an earlier version and would be an API break as well.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
PVE/API2/Cluster.pm | 62 ++++++++++++++++++++++++++++++++++++++++-
PVE/Service/pvestatd.pm | 17 +++++++++++
2 files changed, 78 insertions(+), 1 deletion(-)
diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm
index 479803960..34523fd7e 100644
--- a/PVE/API2/Cluster.pm
+++ b/PVE/API2/Cluster.pm
@@ -251,7 +251,8 @@ __PACKAGE__->register_method({
type => {
description => "Resource type.",
type => 'string',
- enum => ['node', 'storage', 'pool', 'qemu', 'lxc', 'openvz', 'sdn'],
+ enum =>
+ ['node', 'storage', 'pool', 'qemu', 'lxc', 'openvz', 'sdn', 'network'],
},
status => {
description => "Resource type dependent status.",
@@ -431,6 +432,23 @@ __PACKAGE__->register_method({
optional => 1,
default => 0,
},
+ network => {
+ description => "The name of a Network entity (for type 'network').",
+ type => "string",
+ optional => 1,
+ },
+ network_type => {
+ description => "The type of network resource (for type 'network').",
+ type => "string",
+ enum => ["fabric"],
+ optional => 1,
+ },
+ protocol => {
+ description =>
+ "The protocol of a fabric (for type 'network', network_type 'fabric').",
+ type => "string",
+ optional => 1,
+ },
},
},
},
@@ -620,6 +638,48 @@ __PACKAGE__->register_method({
}
}
+ if (!$param->{type} || $param->{type} eq 'network') {
+ my $nodes = PVE::Cluster::get_node_kv("network");
+
+ for my $node (sort keys $nodes->%*) {
+ my $node_config = decode_json($nodes->{$node});
+
+ for my $id (sort keys $node_config->%*) {
+ my $entry = $node_config->{$id};
+
+ if ($entry->{network_type} eq 'fabric') {
+ next
+ if !$rpcenv->check_any(
+ $authuser,
+ "/sdn/fabrics/$entry->{network}",
+ ['SDN.Audit', 'SDN.Allocate'],
+ 1,
+ );
+ } elsif ($entry->{network_type} eq 'zone') {
+ next
+ if !$rpcenv->check(
+ $authuser,
+ "/sdn/zones/$entry->{network}",
+ ['SDN.Audit'],
+ 1,
+ );
+ } else {
+ # unknown type, so most likely introduced in a newer
+ # version - avoid leaking information by suppressing any
+ # unknown sdn types in the returned array.
+ next;
+ }
+
+ push $res->@*,
+ {
+ "id" => "network/$node/$entry->{network_type}/$entry->{network}",
+ "node" => $node,
+ $entry->%*,
+ };
+ }
+ }
+ }
+
return $res;
},
});
diff --git a/PVE/Service/pvestatd.pm b/PVE/Service/pvestatd.pm
index 618d6139a..862bf8b43 100755
--- a/PVE/Service/pvestatd.pm
+++ b/PVE/Service/pvestatd.pm
@@ -15,6 +15,7 @@ use PVE::CpuSet;
use Filesys::Df;
use PVE::INotify;
use PVE::Network;
+use PVE::RS::SDN::Fabrics;
use PVE::NodeConfig;
use PVE::Cluster qw(cfs_read_file);
use PVE::Storage;
@@ -775,6 +776,18 @@ sub update_sdn_status {
}
}
+sub update_network_status {
+ my ($fabric_status) = PVE::RS::SDN::Fabrics::status();
+
+ my $network_status = {};
+
+ for my $fabric (values $fabric_status->%*) {
+ $network_status->{"fabric/$fabric->{network}"} = $fabric;
+ }
+
+ PVE::Cluster::broadcast_node_kv("network", encode_json($network_status));
+}
+
my $broadcast_version_info_done = 0;
my sub broadcast_version_info : prototype() {
if (
@@ -840,6 +853,10 @@ sub update_status {
$err = $@;
syslog('err', "sdn status update error: $err") if $err;
+ eval { update_network_status(); };
+ $err = $@;
+ syslog('err', "network status update error: $err") if $err;
+
eval { broadcast_version_info(); };
$err = $@;
syslog('err', "version info update error: $err") if $err;
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-manager 4/8] pvestatd: sdn: adapt to changes in status reporting
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (29 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 3/8] pvestatd: add network resource to status reporting Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 5/8] ui: resource tree: add network resource Stefan Hanreich
` (3 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
Since pve-network now includes the fabric status in its top-level
status function call, move pvestatd over to use the zone-specific
function for generating SDN resources instead. Additionally, the
zone_type field has been added in the output of the zone status, so
broadcast and return that field as well. Main use-case for now is
discerning the type of zones, to decide which panels to show in the
SDN browser.
Also, the have_sdn guard can be removed since SDN is pre-installed and
therefore the guard is no longer needed.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
PVE/API2/Cluster.pm | 42 +++++++++++++++++++++++------------------
PVE/Service/pvestatd.pm | 16 ++++------------
2 files changed, 28 insertions(+), 30 deletions(-)
diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm
index 34523fd7e..f8e6ed47c 100644
--- a/PVE/API2/Cluster.pm
+++ b/PVE/API2/Cluster.pm
@@ -443,6 +443,11 @@ __PACKAGE__->register_method({
enum => ["fabric"],
optional => 1,
},
+ zone_type => {
+ description => "The type of an SDN zone (for type 'sdn').",
+ type => "string",
+ optional => 1,
+ },
protocol => {
description =>
"The protocol of a fabric (for type 'network', network_type 'fabric').",
@@ -616,24 +621,25 @@ __PACKAGE__->register_method({
}
}
- if ($have_sdn) {
- my $nodes = PVE::Cluster::get_node_kv("sdn");
-
- for my $node (sort keys %{$nodes}) {
- my $sdns = decode_json($nodes->{$node});
-
- for my $id (sort keys %{$sdns}) {
- next if !$rpcenv->check($authuser, "/sdn/zones/$id", ['SDN.Audit'], 1);
- my $sdn = $sdns->{$id};
- my $entry = {
- id => "sdn/$node/$id",
- sdn => $id,
- node => $node,
- type => 'sdn',
- status => $sdn->{'status'},
- };
- push @$res, $entry;
- }
+ my $nodes = PVE::Cluster::get_node_kv("sdn");
+
+ for my $node (sort keys %{$nodes}) {
+ my $sdns = decode_json($nodes->{$node});
+
+ for my $id (sort keys %{$sdns}) {
+ next if !$rpcenv->check($authuser, "/sdn/zones/$id", ['SDN.Audit'], 1);
+ my $sdn = $sdns->{$id};
+ my $entry = {
+ id => "sdn/$node/$id",
+ sdn => $id,
+ node => $node,
+ type => 'sdn',
+ status => $sdn->{'status'},
+ };
+
+ $entry->{zone_type} = $sdn->{zone_type} if defined($sdn->{zone_type});
+
+ push @$res, $entry;
}
}
}
diff --git a/PVE/Service/pvestatd.pm b/PVE/Service/pvestatd.pm
index 862bf8b43..84e8cf7c0 100755
--- a/PVE/Service/pvestatd.pm
+++ b/PVE/Service/pvestatd.pm
@@ -15,6 +15,7 @@ use PVE::CpuSet;
use Filesys::Df;
use PVE::INotify;
use PVE::Network;
+use PVE::Network::SDN::Zones;
use PVE::RS::SDN::Fabrics;
use PVE::NodeConfig;
use PVE::Cluster qw(cfs_read_file);
@@ -38,12 +39,6 @@ use PVE::Status::Plugin;
use base qw(PVE::Daemon);
-my $have_sdn;
-eval {
- require PVE::Network::SDN;
- $have_sdn = 1;
-};
-
my $opt_debug;
my $restart_request;
@@ -767,13 +762,10 @@ sub update_ceph_metadata {
}
sub update_sdn_status {
+ my ($zone_status, $vnet_status) = PVE::Network::SDN::Zones::status();
- if ($have_sdn) {
- my ($transport_status, $vnet_status) = PVE::Network::SDN::status();
-
- my $status = $transport_status ? encode_json($transport_status) : undef;
- PVE::Cluster::broadcast_node_kv("sdn", $status);
- }
+ my $status = $zone_status ? encode_json($zone_status) : undef;
+ PVE::Cluster::broadcast_node_kv("sdn", $status);
}
sub update_network_status {
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-manager 5/8] ui: resource tree: add network resource
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (30 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 4/8] pvestatd: sdn: adapt to changes in " Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 6/8] ui: sdn browser: Add ip-vrf panel for evpn zones Stefan Hanreich
` (2 subsequent siblings)
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add the newly introduced network resource to the resource tree, so the
the status of fabrics can be displayed in the UI. For this matter, a
new NetworkBrowser widget is added, which is responsible for showing
the contents of a network resource.
The NetworkBrowser widget also contains code for handling the zone
type, which is currently still contained in the sdn resource type.
This ensures a smooth transition when moving the zones into the new
network resource type.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 2 +
www/manager6/Utils.js | 11 ++
www/manager6/Workspace.js | 1 +
www/manager6/sdn/FabricsContentView.js | 77 ++++++++++++
www/manager6/sdn/NetworkBrowser.js | 167 +++++++++++++++++++++++++
www/manager6/tree/ResourceTree.js | 6 +
6 files changed, 264 insertions(+)
create mode 100644 www/manager6/sdn/FabricsContentView.js
create mode 100644 www/manager6/sdn/NetworkBrowser.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 85f9268d1..ba762578e 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -278,6 +278,7 @@ JSSRC= \
qemu/USBEdit.js \
qemu/VirtiofsEdit.js \
sdn/Browser.js \
+ sdn/NetworkBrowser.js \
sdn/ControllerView.js \
sdn/Status.js \
sdn/StatusView.js \
@@ -313,6 +314,7 @@ JSSRC= \
sdn/zones/VlanEdit.js \
sdn/zones/VxlanEdit.js \
sdn/FabricsView.js \
+ sdn/FabricsContentView.js \
sdn/fabrics/Common.js \
sdn/fabrics/InterfacePanel.js \
sdn/fabrics/NodeEdit.js \
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index c48ee0b25..2f812a442 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1276,6 +1276,12 @@ Ext.define('PVE.Utils', {
// templates
objType = 'template';
status = type;
+ } else if (type === 'network') {
+ const network_type_mapping = {
+ fabric: 'fa fa-road',
+ };
+
+ return network_type_mapping[record.network_type] ?? '';
} else if (type === 'storage' && record.content === 'import') {
return 'fa fa-cloud-download';
} else {
@@ -1299,6 +1305,11 @@ Ext.define('PVE.Utils', {
var cls = PVE.Utils.get_object_icon_class(value, record.data);
var fa = '<i class="fa-fw x-grid-icon-custom ' + cls + '"></i> ';
+
+ if (value === 'network') {
+ return fa + record.data.network_type;
+ }
+
return fa + value;
},
diff --git a/www/manager6/Workspace.js b/www/manager6/Workspace.js
index a25746582..9f1e807d5 100644
--- a/www/manager6/Workspace.js
+++ b/www/manager6/Workspace.js
@@ -250,6 +250,7 @@ Ext.define('PVE.StdWorkspace', {
lxc: 'pveLXCConfig',
storage: 'PVE.storage.Browser',
sdn: 'PVE.sdn.Browser',
+ network: 'PVE.network.Browser',
pool: 'pvePoolConfig',
tag: 'pveTagConfig',
};
diff --git a/www/manager6/sdn/FabricsContentView.js b/www/manager6/sdn/FabricsContentView.js
new file mode 100644
index 000000000..47e8bce7f
--- /dev/null
+++ b/www/manager6/sdn/FabricsContentView.js
@@ -0,0 +1,77 @@
+Ext.define('PVE.sdn.FabricRoutesContentView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveSDNFabricRoutesContentView',
+
+ columns: [
+ {
+ header: gettext('Route'),
+ sortable: true,
+ dataIndex: 'route',
+ flex: 1,
+ },
+ {
+ header: gettext('Via'),
+ sortable: true,
+ dataIndex: 'via',
+ renderer: (value) => {
+ if (Ext.isArray(value)) {
+ return value.join('<br>');
+ }
+ return value || '';
+ },
+ flex: 1,
+ },
+ ],
+});
+
+Ext.define('PVE.sdn.FabricNeighborsContentView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveSDNFabricNeighborsContentView',
+
+ columns: [
+ {
+ header: gettext('Neighbor'),
+ sortable: true,
+ dataIndex: 'neighbor',
+ flex: 1,
+ },
+ {
+ header: gettext('Status'),
+ sortable: true,
+ dataIndex: 'status',
+ flex: 0.5,
+ },
+ {
+ header: gettext('Uptime'),
+ sortable: true,
+ dataIndex: 'uptime',
+ flex: 0.5,
+ },
+ ],
+});
+
+Ext.define('PVE.sdn.FabricInterfacesContentView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveSDNFabricInterfacesContentView',
+
+ columns: [
+ {
+ header: gettext('Name'),
+ sortable: true,
+ dataIndex: 'name',
+ flex: 1,
+ },
+ {
+ header: gettext('Type'),
+ sortable: true,
+ dataIndex: 'type',
+ flex: 1,
+ },
+ {
+ header: gettext('State'),
+ sortable: true,
+ dataIndex: 'state',
+ flex: 1,
+ },
+ ],
+});
diff --git a/www/manager6/sdn/NetworkBrowser.js b/www/manager6/sdn/NetworkBrowser.js
new file mode 100644
index 000000000..ad024e6a1
--- /dev/null
+++ b/www/manager6/sdn/NetworkBrowser.js
@@ -0,0 +1,167 @@
+Ext.define('PVE.network.Browser', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.network.Browser',
+
+ initComponent: function () {
+ let me = this;
+ let data = me.pveSelNode.data;
+
+ let node = data.node;
+ if (!node) {
+ throw 'no node name specified';
+ }
+
+ let name = data.network;
+ if (!name) {
+ throw 'no name specified';
+ }
+
+ let networkType = data.network_type;
+ if (!name) {
+ throw 'no type specified';
+ }
+
+ me.items = [];
+
+ if (networkType === 'fabric') {
+ me.onlineHelp = 'pvesdn_config_fabrics';
+
+ me.items.push({
+ nodename: node,
+ fabricId: name,
+ protocol: me.pveSelNode.data.protocol,
+ xtype: 'pveSDNFabricRoutesContentView',
+ title: gettext('Routes'),
+ iconCls: 'fa fa-exchange',
+ itemId: 'routes',
+ width: '100%',
+ store: {
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${node}/sdn/fabrics/${name}/routes`,
+ reader: {
+ type: 'json',
+ rootProperty: 'data',
+ },
+ },
+ autoLoad: true,
+ },
+ });
+
+ me.items.push({
+ nodename: node,
+ fabricId: name,
+ protocol: me.pveSelNode.data.protocol,
+ xtype: 'pveSDNFabricNeighborsContentView',
+ title: gettext('Neighbors'),
+ iconCls: 'fa fa-handshake-o',
+ itemId: 'neighbors',
+ width: '100%',
+ store: {
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${node}/sdn/fabrics/${name}/neighbors`,
+ reader: {
+ type: 'json',
+ rootProperty: 'data',
+ },
+ },
+ autoLoad: true,
+ },
+ });
+
+ me.items.push({
+ nodename: node,
+ fabricId: name,
+ protocol: me.pveSelNode.data.protocol,
+ xtype: 'pveSDNFabricInterfacesContentView',
+ title: gettext('Interfaces'),
+ iconCls: 'fa fa-upload',
+ itemId: 'interfaces',
+ width: '100%',
+ store: {
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${node}/sdn/fabrics/${name}/interfaces`,
+ reader: {
+ type: 'json',
+ rootProperty: 'data',
+ },
+ },
+ autoLoad: true,
+ },
+ });
+ } else if (networkType === 'zone') {
+ const caps = Ext.state.Manager.get('GuiCap');
+
+ me.items.push({
+ nodename: node,
+ zone: name,
+ xtype: 'pveSDNZoneContentPanel',
+ title: gettext('Content'),
+ iconCls: 'fa fa-th',
+ itemId: 'content',
+ });
+
+ if (caps.sdn['Permissions.Modify']) {
+ me.items.push({
+ xtype: 'pveACLView',
+ title: gettext('Permissions'),
+ iconCls: 'fa fa-unlock',
+ itemId: 'permissions',
+ path: `/sdn/zones/${name}`,
+ });
+ }
+
+ me.items.push({
+ nodename: node,
+ zone: name,
+ xtype: 'pveSDNZoneBridgePanel',
+ title: gettext('Bridges'),
+ iconCls: 'fa fa-network-wired x-fa-sdn-treelist',
+ itemId: 'bridges',
+ });
+
+ if (data.zone_type && data.zone_type === 'evpn') {
+ me.items.push({
+ nodename: node,
+ zone: name,
+ xtype: 'pveSDNEvpnZoneIpVrfPanel',
+ title: gettext('IP-VRF'),
+ iconCls: 'fa fa-th-list',
+ itemId: 'ip-vrf',
+ });
+
+ me.items.push({
+ nodename: node,
+ zone: name,
+ xtype: 'pveSDNEvpnZoneMacVrfPanel',
+ title: gettext('MAC-VRFs'),
+ iconCls: 'fa fa-th-list',
+ itemId: 'mac-vrfs',
+ });
+ }
+ } else {
+ me.items.push({
+ xtype: 'container',
+ title: gettext('Content'),
+ iconCls: 'fa fa-th',
+ itemId: 'content',
+ html: `unknown network type: ${networkType}`,
+ width: '100%',
+ });
+ }
+
+ Ext.apply(me, {
+ title: Ext.String.format(
+ gettext('{0} {1} on node {2}'),
+ `${networkType}`,
+ `'${name}'`,
+ `'${node}'`,
+ ),
+ hstateid: 'networktab',
+ });
+
+ me.callParent();
+ },
+});
diff --git a/www/manager6/tree/ResourceTree.js b/www/manager6/tree/ResourceTree.js
index e83ccfc85..b6ab9a1e9 100644
--- a/www/manager6/tree/ResourceTree.js
+++ b/www/manager6/tree/ResourceTree.js
@@ -25,6 +25,10 @@ Ext.define('PVE.tree.ResourceTree', {
iconCls: 'fa fa-th',
text: gettext('SDN'),
},
+ network: {
+ iconCls: 'fa fa-globe',
+ text: gettext('Network'),
+ },
qemu: {
iconCls: 'fa fa-desktop',
text: gettext('Virtual Machine'),
@@ -55,6 +59,8 @@ Ext.define('PVE.tree.ResourceTree', {
return 2;
case 'sdn':
return 3;
+ case 'network':
+ return 3.5;
case 'storage':
return 4;
default:
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-manager 6/8] ui: sdn browser: Add ip-vrf panel for evpn zones
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (31 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 5/8] ui: resource tree: add network resource Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 7/8] ui: sdn browser: add mac vrf panel Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 8/8] ui: sdn browser: add zone bridge view Stefan Hanreich
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
This panel shows the full state of the VRF of an EVPN zone on a
given node. It is integrated into the SDN browser and only shown for
EVPN zones.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/sdn/Browser.js | 11 ++++
www/manager6/sdn/EvpnZoneIpVrfPanel.js | 84 ++++++++++++++++++++++++++
3 files changed, 96 insertions(+)
create mode 100644 www/manager6/sdn/EvpnZoneIpVrfPanel.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index ba762578e..6abc77469 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -290,6 +290,7 @@ JSSRC= \
sdn/SubnetView.js \
sdn/ZoneContentView.js \
sdn/ZoneContentPanel.js \
+ sdn/EvpnZoneIpVrfPanel.js \
sdn/FirewallPanel.js \
sdn/FirewallVnetView.js \
sdn/ZoneView.js \
diff --git a/www/manager6/sdn/Browser.js b/www/manager6/sdn/Browser.js
index f7694ae91..d2e70f32a 100644
--- a/www/manager6/sdn/Browser.js
+++ b/www/manager6/sdn/Browser.js
@@ -48,6 +48,17 @@ Ext.define('PVE.sdn.Browser', {
});
}
+ if (me.pveSelNode.data.zone_type && me.pveSelNode.data.zone_type === 'evpn') {
+ me.items.push({
+ nodename: nodename,
+ zone: sdnId,
+ xtype: 'pveSDNEvpnZoneIpVrfPanel',
+ title: gettext('IP-VRF'),
+ iconCls: 'fa fa-th-list',
+ itemId: 'ip-vrf',
+ });
+ }
+
me.callParent();
},
});
diff --git a/www/manager6/sdn/EvpnZoneIpVrfPanel.js b/www/manager6/sdn/EvpnZoneIpVrfPanel.js
new file mode 100644
index 000000000..2b9ded537
--- /dev/null
+++ b/www/manager6/sdn/EvpnZoneIpVrfPanel.js
@@ -0,0 +1,84 @@
+Ext.define('IpVrfRoute', {
+ extend: 'Ext.data.Model',
+ fields: ['ip', 'metric', 'nexthops', 'protocol'],
+});
+
+Ext.define('PVE.sdn.EvpnZoneIpVrfPanel', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveSDNEvpnZoneIpVrfPanel',
+
+ title: gettext('IP-VRF'),
+ onlineHelp: 'pvesdn_zone_plugin_evpn',
+
+ stateful: true,
+ stateId: 'grid-sdn-ip-vrf',
+
+ columns: [
+ {
+ text: gettext('CIDR'),
+ flex: 2,
+ sortable: true,
+ dataIndex: 'ip',
+ },
+ {
+ text: gettext('Nexthop'),
+ flex: 3,
+ dataIndex: 'nexthops',
+ renderer: (value) => {
+ if (Ext.isArray(value)) {
+ return value.join('<br>');
+ }
+ return value || '';
+ },
+ },
+ {
+ text: gettext('Protocol'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'protocol',
+ },
+ {
+ text: gettext('Metric'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'metric',
+ },
+ ],
+
+ initComponent: function () {
+ let me = this;
+
+ let store = new Ext.data.Store({
+ model: 'IpVrfRoute',
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${me.nodename}/sdn/zones/${me.zone}/ip-vrf`,
+ reader: {
+ type: 'json',
+ rootProperty: 'data',
+ },
+ },
+ sorters: [
+ {
+ property: 'ip',
+ direction: 'ASC',
+ },
+ {
+ property: 'nexthop',
+ direction: 'ASC',
+ },
+ {
+ property: 'metric',
+ direction: 'ASC',
+ },
+ ],
+ autoLoad: true,
+ });
+
+ Ext.apply(me, {
+ store,
+ });
+
+ me.callParent();
+ },
+});
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-manager 7/8] ui: sdn browser: add mac vrf panel
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (32 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 6/8] ui: sdn browser: Add ip-vrf panel for evpn zones Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 8/8] ui: sdn browser: add zone bridge view Stefan Hanreich
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
This panel shows the full state of the neighbor table of an EVPN vnet.
It is integrated into the SDN browser and only shown for EVPN zones.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/sdn/Browser.js | 9 ++
www/manager6/sdn/EvpnZoneMacVrfPanel.js | 130 ++++++++++++++++++++++++
3 files changed, 140 insertions(+)
create mode 100644 www/manager6/sdn/EvpnZoneMacVrfPanel.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 6abc77469..3f7125e1f 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -291,6 +291,7 @@ JSSRC= \
sdn/ZoneContentView.js \
sdn/ZoneContentPanel.js \
sdn/EvpnZoneIpVrfPanel.js \
+ sdn/EvpnZoneMacVrfPanel.js \
sdn/FirewallPanel.js \
sdn/FirewallVnetView.js \
sdn/ZoneView.js \
diff --git a/www/manager6/sdn/Browser.js b/www/manager6/sdn/Browser.js
index d2e70f32a..8960caf2d 100644
--- a/www/manager6/sdn/Browser.js
+++ b/www/manager6/sdn/Browser.js
@@ -57,6 +57,15 @@ Ext.define('PVE.sdn.Browser', {
iconCls: 'fa fa-th-list',
itemId: 'ip-vrf',
});
+
+ me.items.push({
+ nodename: nodename,
+ zone: sdnId,
+ xtype: 'pveSDNEvpnZoneMacVrfPanel',
+ title: gettext('MAC-VRFs'),
+ iconCls: 'fa fa-th-list',
+ itemId: 'mac-vrfs',
+ });
}
me.callParent();
diff --git a/www/manager6/sdn/EvpnZoneMacVrfPanel.js b/www/manager6/sdn/EvpnZoneMacVrfPanel.js
new file mode 100644
index 000000000..0984fdbdc
--- /dev/null
+++ b/www/manager6/sdn/EvpnZoneMacVrfPanel.js
@@ -0,0 +1,130 @@
+Ext.define('PVE.sdn.EvpnZoneMacVrfPanel', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveSDNEvpnZoneMacVrfPanel',
+
+ title: 'MAC-VRFs',
+ onlineHelp: 'pvesdn_zone_plugin_evpn',
+
+ initComponent: function () {
+ var me = this;
+ let nodename = me.nodename;
+
+ var mac_vrf_panel = Ext.createWidget('pveSDNEvpnZoneMacVrfGridPanel', {
+ title: gettext('VNet MAC-VRF'),
+ region: 'center',
+ border: false,
+ });
+
+ var vnetview_panel = Ext.createWidget('pveSDNZoneContentView', {
+ title: gettext('VNets'),
+ region: 'west',
+ sub_panel: mac_vrf_panel,
+ nodename: me.nodename,
+ zone: me.zone,
+
+ width: '50%',
+ border: false,
+ split: true,
+
+ on_select: function (_sm, rec) {
+ mac_vrf_panel.setVnet(rec.data.vnet, nodename);
+ },
+
+ on_deselect: function () {
+ mac_vrf_panel.clearVnet();
+ },
+ });
+
+ Ext.apply(me, {
+ layout: 'border',
+ items: [vnetview_panel, mac_vrf_panel],
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('MacVrfRoute', {
+ extend: 'Ext.data.Model',
+ fields: ['ip', 'metric', 'nexthops', 'protocol'],
+});
+
+Ext.define('PVE.sdn.EvpnZoneMacVrfGridPanel', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveSDNEvpnZoneMacVrfGridPanel',
+
+ title: gettext('MAC-VRF'),
+
+ stateful: true,
+ stateId: 'grid-sdn-mac-vrf',
+
+ columns: [
+ {
+ text: gettext('IP'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'ip',
+ },
+ {
+ text: gettext('MAC-Address'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'mac',
+ },
+ {
+ text: gettext('Nexthop'),
+ flex: 1,
+ dataIndex: 'nexthop',
+ },
+ ],
+
+ clearVnet: function () {
+ let me = this;
+
+ me.getStore().removeAll();
+ },
+
+ setVnet: function (vnet, node) {
+ let me = this;
+
+ let store = me.getStore();
+
+ store.getProxy().setUrl(`/api2/json/nodes/${node}/sdn/vnets/${vnet}/mac-vrf`);
+ store.load();
+ },
+
+ initComponent: function () {
+ let me = this;
+
+ let store = new Ext.data.Store({
+ model: 'MacVrfRoute',
+ proxy: {
+ type: 'proxmox',
+ reader: {
+ type: 'json',
+ rootProperty: 'data',
+ },
+ },
+ sorters: [
+ {
+ property: 'ip',
+ direction: 'ASC',
+ },
+ {
+ property: 'mac',
+ direction: 'ASC',
+ },
+ {
+ property: 'nexthop',
+ direction: 'ASC',
+ },
+ ],
+ });
+
+ Ext.apply(me, {
+ store,
+ });
+
+ me.callParent();
+ },
+});
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
* [pve-devel] [PATCH pve-manager 8/8] ui: sdn browser: add zone bridge view
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
` (33 preceding siblings ...)
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 7/8] ui: sdn browser: add mac vrf panel Stefan Hanreich
@ 2025-10-30 15:48 ` Stefan Hanreich
34 siblings, 0 replies; 36+ messages in thread
From: Stefan Hanreich @ 2025-10-30 15:48 UTC (permalink / raw)
To: pve-devel
This shows a list of all bridges, that are part of an SDN zone
(including the pseudo localnetwork zone, that corresponds to the local
network of a node). It also shows additional information about which
ports are currently members of the bridge, as well as their VLAN
configuration and which guest they belong to.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 2 +
www/manager6/sdn/Browser.js | 9 ++
www/manager6/sdn/ZoneBridgeView.js | 88 ++++++++++++++++++
www/manager6/sdn/ZoneBridgesPanel.js | 131 +++++++++++++++++++++++++++
4 files changed, 230 insertions(+)
create mode 100644 www/manager6/sdn/ZoneBridgeView.js
create mode 100644 www/manager6/sdn/ZoneBridgesPanel.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 3f7125e1f..68c3f4457 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -290,6 +290,8 @@ JSSRC= \
sdn/SubnetView.js \
sdn/ZoneContentView.js \
sdn/ZoneContentPanel.js \
+ sdn/ZoneBridgesPanel.js \
+ sdn/ZoneBridgeView.js \
sdn/EvpnZoneIpVrfPanel.js \
sdn/EvpnZoneMacVrfPanel.js \
sdn/FirewallPanel.js \
diff --git a/www/manager6/sdn/Browser.js b/www/manager6/sdn/Browser.js
index 8960caf2d..4dc709ff0 100644
--- a/www/manager6/sdn/Browser.js
+++ b/www/manager6/sdn/Browser.js
@@ -48,6 +48,15 @@ Ext.define('PVE.sdn.Browser', {
});
}
+ me.items.push({
+ nodename: nodename,
+ zone: sdnId,
+ xtype: 'pveSDNZoneBridgePanel',
+ title: gettext('Bridges'),
+ iconCls: 'fa fa-network-wired x-fa-sdn-treelist',
+ itemId: 'bridges',
+ });
+
if (me.pveSelNode.data.zone_type && me.pveSelNode.data.zone_type === 'evpn') {
me.items.push({
nodename: nodename,
diff --git a/www/manager6/sdn/ZoneBridgeView.js b/www/manager6/sdn/ZoneBridgeView.js
new file mode 100644
index 000000000..316ce86f2
--- /dev/null
+++ b/www/manager6/sdn/ZoneBridgeView.js
@@ -0,0 +1,88 @@
+Ext.define('ZoneBridge', {
+ extend: 'Ext.data.Model',
+ fields: ['name', 'vlan_filtering', 'ports'],
+});
+
+Ext.define('PVE.sdn.ZoneBridgeView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveSDNZoneBridgeView',
+
+ stateful: true,
+ stateId: 'grid-sdnzone-bridges',
+
+ viewConfig: {
+ trackOver: false,
+ loadMask: false,
+ },
+
+ columns: [
+ {
+ header: gettext('Bridge'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'name',
+ flex: 1,
+ },
+ {
+ header: gettext('VLAN-aware'),
+ width: 300,
+ sortable: true,
+ dataIndex: 'vlan_filtering',
+ flex: 1,
+ renderer: function (value) {
+ return value === 1 ? gettext('Yes') : gettext('No');
+ },
+ },
+ ],
+
+ on_select: function (selectionModel, record) {
+ // do nothing by default
+ },
+
+ on_deselect: function () {
+ // do nothing by default
+ },
+
+ initComponent: function () {
+ var me = this;
+
+ if (!me.nodename) {
+ throw 'no node name specified';
+ }
+
+ if (!me.zone) {
+ throw 'no zone ID specified';
+ }
+
+ let baseUrl = `/nodes/${me.nodename}/sdn/zones/${me.zone}/bridges`;
+
+ let store = Ext.create('Ext.data.Store', {
+ model: 'ZoneBridge',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json' + baseUrl,
+ },
+ sorters: {
+ property: 'name',
+ direction: 'ASC',
+ },
+ });
+
+ let reload = function () {
+ store.load();
+ };
+
+ Proxmox.Utils.monStoreErrors(me, store);
+ Ext.apply(me, {
+ store: store,
+ listeners: {
+ activate: reload,
+ show: reload,
+ select: me.on_select,
+ deselect: me.on_deselect,
+ },
+ });
+ store.load();
+ me.callParent();
+ },
+});
diff --git a/www/manager6/sdn/ZoneBridgesPanel.js b/www/manager6/sdn/ZoneBridgesPanel.js
new file mode 100644
index 000000000..600b23b0a
--- /dev/null
+++ b/www/manager6/sdn/ZoneBridgesPanel.js
@@ -0,0 +1,131 @@
+Ext.define('PVE.sdn.ZoneBridgePanel', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveSDNZoneBridgePanel',
+
+ title: gettext('Bridges'),
+ onlineHelp: 'pvesdn_zone_plugin_evpn',
+
+ stateful: true,
+ stateId: 'grid-sdn-zone-bridges',
+
+ initComponent: function () {
+ var me = this;
+ let nodename = me.nodename;
+
+ var bridge_ports_panel = Ext.createWidget('pveSDNZoneBridgePortsPanel', {
+ title: gettext('Bridge Ports'),
+ region: 'center',
+ border: false,
+ });
+
+ var vnetview_panel = Ext.createWidget('pveSDNZoneBridgeView', {
+ title: gettext('VNets'),
+ region: 'west',
+ nodename: me.nodename,
+ zone: me.zone,
+
+ width: '50%',
+ border: false,
+ split: true,
+
+ on_select: function (_sm, rec) {
+ let deepCopy = structuredClone(rec.data.ports);
+ bridge_ports_panel.setPorts(deepCopy, nodename);
+ },
+
+ on_deselect: function () {
+ bridge_ports_panel.clearPorts();
+ },
+ });
+
+ Ext.apply(me, {
+ layout: 'border',
+ items: [vnetview_panel, bridge_ports_panel],
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('ZoneBridgePort', {
+ extend: 'Ext.data.Model',
+ fields: ['index', 'name', 'primary_vlan', 'vlans', 'vmid'],
+});
+
+Ext.define('PVE.sdn.ZoneBridgePortsPanel', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveSDNZoneBridgePortsPanel',
+
+ title: gettext('IP-VRF'),
+ onlineHelp: 'pvesdn_zone_plugin_evpn',
+
+ stateful: true,
+ stateId: 'grid-sdn-zone-ports',
+
+ columns: [
+ {
+ text: gettext('Name'),
+ flex: 2,
+ sortable: true,
+ dataIndex: 'name',
+ },
+ {
+ text: gettext('VMID'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'vmid',
+ },
+ {
+ text: gettext('Network Device Index'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'index',
+ },
+ {
+ text: gettext('Primary VLAN'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'primary_vlan',
+ },
+ {
+ text: gettext('VLANs'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'vlans',
+ },
+ ],
+
+ initComponent: function () {
+ let me = this;
+
+ let store = new Ext.data.Store({
+ model: 'ZoneBridge',
+ sorters: [
+ {
+ property: 'vmid',
+ direction: 'ASC',
+ },
+ {
+ property: 'index',
+ direction: 'ASC',
+ },
+ ],
+ });
+
+ Ext.apply(me, {
+ store,
+ });
+
+ me.callParent();
+ },
+
+ setPorts: function (ports) {
+ let me = this;
+ me.getStore().setData(ports);
+ },
+
+ clearPorts: function (ports) {
+ let me = this;
+ me.getStore().removeAll();
+ },
+});
--
2.47.3
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 36+ messages in thread
end of thread, other threads:[~2025-10-30 15:59 UTC | newest]
Thread overview: 36+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-10-30 15:48 [pve-devel] [PATCH common/manager/network/proxmox{-ve-rs, -perl-rs} 00/35] Improve status reporting for SDN / networking Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-common 1/2] iproute2: add helper for detecting bridge members Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-common 2/2] iproute2: add helper for querying vlan information Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 1/6] frr: make room for deserialization structs Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 2/6] frr: add deserialization types for openfabric and ospf Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 3/6] ve-config: add helper function to iterate over all nodes in all fabrics Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 4/6] ve-config: add optional tag property to vnet Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 5/6] frr: fix some route deserialization types Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-ve-rs 6/6] frr: add deserialization types for EVPN Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 01/10] pve-rs: firewall: cargo: fmt Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 02/10] pve-rs: firewall: add missing documentation comments Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 03/10] pve-rs: cargo: bump proxmox-apt and proxmox-ve-config versions Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 04/10] pve-rs: fabrics: update proxmox-frr import path Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 05/10] pve-rs: fabrics: fix clippy lint warnings Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 06/10] pve-rs: fabrics: add function to get status of fabric Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 07/10] pve-rs: fabrics: add function to get l2vpn and l3vpn routes for evpn Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 08/10] pve-rs: fabrics: add function to get routes learned by a fabric Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 09/10] pve-rs: fabrics: add function to get the interfaces used for " Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH proxmox-perl-rs 10/10] pve-rs: fabrics: add function to get the neighbors " Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 1/9] refactor: rework api module structure for the /nodes/{node}/sdn subdir Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 2/9] fabrics: add fabrics status to SDN::status function Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 3/9] sdn: status: add zone type to sdn resource Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 4/9] api: nodes: fabrics: add endpoint for querying route status Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 5/9] api: nodes: fabrics: add endpoint for querying neighbor information Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 6/9] api: nodes: fabrics: add endpoint for querying interface status Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 7/9] api: nodes: zones: add bridge status Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 8/9] api: nodes: zones: add ip vrf endpoint for evpn zones Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-network 9/9] api: nodes: vnets: add mac-vrf endpoint for evpn vnets Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 1/8] api: nodes: use new status module for sdn subdirectory Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 2/8] refactor: ui: sdn browser: parametrize zone content panel Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 3/8] pvestatd: add network resource to status reporting Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 4/8] pvestatd: sdn: adapt to changes in " Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 5/8] ui: resource tree: add network resource Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 6/8] ui: sdn browser: Add ip-vrf panel for evpn zones Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 7/8] ui: sdn browser: add mac vrf panel Stefan Hanreich
2025-10-30 15:48 ` [pve-devel] [PATCH pve-manager 8/8] ui: sdn browser: add zone bridge view Stefan Hanreich
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox