all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH proxmox-datacenter-manager 1/3] resources: support new pve network resource type
Date: Fri,  7 Nov 2025 15:40:09 +0100	[thread overview]
Message-ID: <20251107144018.700695-5-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20251107144018.700695-1-s.hanreich@proxmox.com>

Proxmox VE now returns the new network resource type, in addition to
the sdn resource type. This patch adds support for this new format.

Since PDM makes one API call to a single remote (`/cluster/resources`)
to obtain the resources, handling of the different resource types is
already done by the Proxmox VE remote. If it is on a version that uses
the sdn resource type, then only sdn resources will get returned and
vice vers.

For that reason, the whole output can simply be deserialized and used
without any special case handling on PDM side. Both resource types get
mapped to the same PDM-internal struct. An additional legacy flag has
been added to the zone resource, in order for us to be able to
distinguish whether the resource came from an older or newer PVE
version. This is required to generate the correct URLs for the
resources in the frontend, among other things.

Distinguishing between different types of network entities, now
happens via the network_type, field of network resources. Instead of
returning a specific type like `sdn-zone`, use the same type for all
network resources and provide filtering. This will be implemented in a
successive patch.

There is no need for providing a backwards-compatible Resource struct
for deserialization purposes, since the only place where the current
sdn resources are used, use the ephemeral resource cache - which gets
deleted upon restarting the service.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 cli/client/src/resources.rs                  |   9 +-
 lib/pdm-api-types/src/resource.rs            | 115 +++++++++++++------
 lib/pdm-client/src/lib.rs                    |   2 +
 server/src/api/resources.rs                  |  87 ++++++++++----
 server/src/metric_collection/top_entities.rs |   2 +-
 5 files changed, 158 insertions(+), 57 deletions(-)

diff --git a/cli/client/src/resources.rs b/cli/client/src/resources.rs
index dbf9f26..2fc30e2 100644
--- a/cli/client/src/resources.rs
+++ b/cli/client/src/resources.rs
@@ -52,7 +52,7 @@ async fn get_resources(max_age: Option<u64>) -> Result<(), Error> {
                     Resource::PveQemu(r) => println!("{}", PrintResource(r)),
                     Resource::PveLxc(r) => println!("{}", PrintResource(r)),
                     Resource::PveNode(r) => println!("{}", PrintResource(r)),
-                    Resource::PveSdn(r) => println!("{}", PrintResource(r)),
+                    Resource::PveNetwork(r) => println!("{}", PrintResource(r)),
                     Resource::PbsNode(r) => println!("{}", PrintResource(r)),
                     Resource::PbsDatastore(r) => println!("{}", PrintResource(r)),
                 }
@@ -70,7 +70,7 @@ fn resource_order(item: &Resource) -> usize {
         Resource::PveStorage(_) => 1,
         Resource::PveLxc(_) => 2,
         Resource::PveQemu(_) => 3,
-        Resource::PveSdn(_) => 4,
+        Resource::PveNetwork(_) => 4,
 
         Resource::PbsNode(_) => 0,
         Resource::PbsDatastore(_) => 1,
@@ -150,11 +150,12 @@ impl fmt::Display for PrintResource<resource::PveNodeResource> {
     }
 }
 
-impl fmt::Display for PrintResource<resource::PveSdnResource> {
+impl fmt::Display for PrintResource<resource::PveNetworkResource> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(
             f,
-            "    sdn zone {name} ({status}) on {node}",
+            "    network {network_type} {name} ({status}) on {node}",
+            network_type = self.0.network_type(),
             name = self.0.name(),
             status = self.0.status(),
             node = self.0.node(),
diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
index e09678d..d89b45d 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
 use proxmox_schema::{api, ApiStringFormat, ApiType, EnumEntry, OneOfSchema, Schema, StringSchema};
 
 use super::remotes::{RemoteType, REMOTE_ID_SCHEMA};
+use pve_api_types::ClusterResourceNetworkType;
 
 /// High PBS datastore usage threshold
 pub const PBS_DATASTORE_HIGH_USAGE_THRESHOLD: f64 = 0.80;
@@ -25,7 +26,7 @@ pub enum Resource {
     PveQemu(PveQemuResource),
     PveLxc(PveLxcResource),
     PveNode(PveNodeResource),
-    PveSdn(PveSdnResource),
+    PveNetwork(PveNetworkResource),
     PbsNode(PbsNodeResource),
     PbsDatastore(PbsDatastoreResource),
 }
@@ -39,7 +40,15 @@ impl Resource {
             Resource::PveQemu(r) => format!("qemu/{}", r.vmid),
             Resource::PveLxc(r) => format!("lxc/{}", r.vmid),
             Resource::PveNode(r) => format!("node/{}", r.node),
-            Resource::PveSdn(PveSdnResource::Zone(r)) => format!("sdn/{}/{}", r.node, r.name),
+            Resource::PveNetwork(r) => {
+                if let PveNetworkResource::Zone(z) = r {
+                    if z.legacy {
+                        return format!("sdn/{}/{}", r.node(), r.name());
+                    }
+                }
+
+                format!("network/{}/{}/{}", r.node(), r.network_type(), r.name())
+            }
             Resource::PbsNode(r) => format!("node/{}", r.name),
             Resource::PbsDatastore(r) => r.name.clone(),
         }
@@ -53,7 +62,7 @@ impl Resource {
             Resource::PveQemu(r) => r.id.as_str(),
             Resource::PveLxc(r) => r.id.as_str(),
             Resource::PveNode(r) => r.id.as_str(),
-            Resource::PveSdn(r) => r.id(),
+            Resource::PveNetwork(r) => r.id(),
             Resource::PbsNode(r) => r.id.as_str(),
             Resource::PbsDatastore(r) => r.id.as_str(),
         }
@@ -67,7 +76,7 @@ impl Resource {
             Resource::PveQemu(r) => r.name.as_str(),
             Resource::PveLxc(r) => r.name.as_str(),
             Resource::PveNode(r) => r.node.as_str(),
-            Resource::PveSdn(r) => r.name(),
+            Resource::PveNetwork(r) => r.name(),
             Resource::PbsNode(r) => r.name.as_str(),
             Resource::PbsDatastore(r) => r.name.as_str(),
         }
@@ -78,7 +87,7 @@ impl Resource {
             Resource::PveStorage(_) => ResourceType::PveStorage,
             Resource::PveQemu(_) => ResourceType::PveQemu,
             Resource::PveLxc(_) => ResourceType::PveLxc,
-            Resource::PveSdn(PveSdnResource::Zone(_)) => ResourceType::PveSdnZone,
+            Resource::PveNetwork(_) => ResourceType::PveNetwork,
             Resource::PveNode(_) | Resource::PbsNode(_) => ResourceType::Node,
             Resource::PbsDatastore(_) => ResourceType::PbsDatastore,
         }
@@ -90,7 +99,7 @@ impl Resource {
             Resource::PveQemu(r) => r.status.as_str(),
             Resource::PveLxc(r) => r.status.as_str(),
             Resource::PveNode(r) => r.status.as_str(),
-            Resource::PveSdn(r) => r.status().as_str(),
+            Resource::PveNetwork(r) => r.status(),
             Resource::PbsNode(r) => {
                 if r.uptime > 0 {
                     "online"
@@ -138,9 +147,9 @@ pub enum ResourceType {
     /// PVE LXC Resource
     #[serde(rename = "lxc")]
     PveLxc,
-    /// PVE SDN Resource
-    #[serde(rename = "sdn-zone")]
-    PveSdnZone,
+    /// PVE Network Resource
+    #[serde(rename = "network")]
+    PveNetwork,
     /// PBS Datastore Resource
     #[serde(rename = "datastore")]
     PbsDatastore,
@@ -156,7 +165,7 @@ impl ResourceType {
             ResourceType::PveStorage => "storage",
             ResourceType::PveQemu => "qemu",
             ResourceType::PveLxc => "lxc",
-            ResourceType::PveSdnZone => "sdn-zone",
+            ResourceType::PveNetwork => "network",
             ResourceType::PbsDatastore => "datastore",
             ResourceType::Node => "node",
         }
@@ -177,7 +186,7 @@ impl std::str::FromStr for ResourceType {
             "storage" => ResourceType::PveStorage,
             "qemu" => ResourceType::PveQemu,
             "lxc" => ResourceType::PveLxc,
-            "sdn-zone" => ResourceType::PveSdnZone,
+            "network" => ResourceType::PveNetwork,
             "datastore" => ResourceType::PbsDatastore,
             "node" => ResourceType::Node,
             _ => bail!("invalid resource type"),
@@ -201,7 +210,7 @@ pub enum PveResource {
     Qemu(PveQemuResource),
     Lxc(PveLxcResource),
     Node(PveNodeResource),
-    Sdn(PveSdnResource),
+    Network(PveNetworkResource),
 }
 
 #[api(
@@ -352,15 +361,48 @@ pub struct PveStorageResource {
 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
 #[serde(rename_all = "kebab-case")]
 /// SDN Zone
-pub struct SdnZoneResource {
+pub struct NetworkZoneResource {
     /// Resource ID
     pub id: String,
+    /// Cluster node name
+    pub node: String,
     /// Name of the resource
-    pub name: String,
+    pub network: String,
+    /// SDN status (available / error)
+    pub status: SdnStatus,
+    /// Zone type
+    pub zone_type: String,
+    /// legacy
+    pub legacy: bool,
+}
+
+impl NetworkZoneResource {
+    pub fn status(&self) -> SdnStatus {
+        self.status
+    }
+}
+
+#[api]
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// SDN Fabric
+pub struct NetworkFabricResource {
+    /// Resource ID
+    pub id: String,
     /// Cluster node name
     pub node: String,
+    /// Name of the resource
+    pub network: String,
     /// SDN status (available / error)
     pub status: SdnStatus,
+    /// faabric protocol
+    pub protocol: String,
+}
+
+impl NetworkFabricResource {
+    pub fn status(&self) -> SdnStatus {
+        self.status
+    }
 }
 
 #[derive(Clone, Debug, Serialize, PartialEq, Copy, Default)]
@@ -407,58 +449,67 @@ impl ApiType for SdnStatus {
 }
 
 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
-#[serde(tag = "sdn_type", rename_all = "lowercase")]
+#[serde(tag = "network_type", rename_all = "lowercase")]
 /// SDN resource in PDM
-pub enum PveSdnResource {
-    Zone(SdnZoneResource),
+pub enum PveNetworkResource {
+    Fabric(NetworkFabricResource),
+    Zone(NetworkZoneResource),
 }
 
-impl ApiType for PveSdnResource {
+impl ApiType for PveNetworkResource {
     const API_SCHEMA: Schema = OneOfSchema::new(
-        "PVE SDN resource",
+        "PVE Network resource",
         &(
-            "sdn_type",
+            "network_type",
             false,
-            &StringSchema::new("PVE SDN resource type")
-                .format(&ApiStringFormat::Enum(&[EnumEntry::new(
-                    "zone",
-                    "An SDN zone.",
-                )]))
+            &StringSchema::new("PVE Network resource type")
+                .format(&ApiStringFormat::Enum(&[
+                    EnumEntry::new("zone", "An SDN zone."),
+                    EnumEntry::new("fabric", "An SDN fabric."),
+                ]))
                 .schema(),
         ),
-        &[("zone", &SdnZoneResource::API_SCHEMA)],
+        &[
+            ("fabric", &NetworkFabricResource::API_SCHEMA),
+            ("zone", &NetworkZoneResource::API_SCHEMA),
+        ],
     )
     .schema();
 }
 
-impl PveSdnResource {
+impl PveNetworkResource {
     pub fn id(&self) -> &str {
         match self {
             Self::Zone(zone) => zone.id.as_str(),
+            Self::Fabric(fabric) => fabric.id.as_str(),
         }
     }
 
     pub fn name(&self) -> &str {
         match self {
-            Self::Zone(zone) => zone.name.as_str(),
+            Self::Zone(zone) => zone.network.as_str(),
+            Self::Fabric(fabric) => fabric.network.as_str(),
         }
     }
 
     pub fn node(&self) -> &str {
         match self {
             Self::Zone(zone) => zone.node.as_str(),
+            Self::Fabric(fabric) => fabric.node.as_str(),
         }
     }
 
-    pub fn status(&self) -> SdnStatus {
+    pub fn status(&self) -> &str {
         match self {
-            Self::Zone(zone) => zone.status,
+            Self::Zone(zone) => zone.status.as_str(),
+            Self::Fabric(fabric) => fabric.status.as_str(),
         }
     }
 
-    pub fn sdn_type(&self) -> &'static str {
+    pub fn network_type(&self) -> ClusterResourceNetworkType {
         match self {
-            Self::Zone(_) => "sdn-zone",
+            Self::Zone(_) => ClusterResourceNetworkType::Zone,
+            Self::Fabric(_) => ClusterResourceNetworkType::Fabric,
         }
     }
 }
diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 0cab769..dd1eb02 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -65,6 +65,8 @@ pub mod types {
     };
     pub use pve_api_types::{ListControllersType, ListZonesType, SdnObjectState};
 
+    pub use pve_api_types::ClusterResourceNetworkType;
+
     pub use pve_api_types::StorageStatus as PveStorageStatus;
 }
 
diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs
index dad3e6b..0204f95 100644
--- a/server/src/api/resources.rs
+++ b/server/src/api/resources.rs
@@ -11,9 +11,10 @@ use pbs_api_types::{
 };
 use pdm_api_types::remotes::{Remote, RemoteType};
 use pdm_api_types::resource::{
-    FailedRemote, PbsDatastoreResource, PbsNodeResource, PveLxcResource, PveNodeResource,
-    PveQemuResource, PveSdnResource, PveStorageResource, RemoteResources, Resource, ResourceType,
-    ResourcesStatus, SdnStatus, SdnZoneResource, TopEntities, PBS_DATASTORE_HIGH_USAGE_THRESHOLD,
+    FailedRemote, NetworkFabricResource, NetworkZoneResource, PbsDatastoreResource,
+    PbsNodeResource, PveLxcResource, PveNetworkResource, PveNodeResource, PveQemuResource,
+    PveStorageResource, RemoteResources, Resource, ResourceType, ResourcesStatus, SdnStatus,
+    TopEntities, PBS_DATASTORE_HIGH_USAGE_THRESHOLD,
 };
 use pdm_api_types::subscription::{
     NodeSubscriptionInfo, RemoteSubscriptionState, RemoteSubscriptions, SubscriptionLevel,
@@ -28,7 +29,7 @@ use proxmox_rrd_api_types::RrdTimeframe;
 use proxmox_schema::{api, parse_boolean};
 use proxmox_sortable_macro::sortable;
 use proxmox_subscription::SubscriptionStatus;
-use pve_api_types::{ClusterResource, ClusterResourceType};
+use pve_api_types::{ClusterResource, ClusterResourceNetworkType, ClusterResourceType};
 
 use crate::connection;
 use crate::metric_collection::top_entities;
@@ -441,18 +442,18 @@ pub async fn get_status(
                     "offline" => counts.pve_nodes.offline += 1,
                     _ => counts.pve_nodes.unknown += 1,
                 },
-                Resource::PveSdn(r) => {
-                    let PveSdnResource::Zone(_) = &r;
-
-                    match r.status() {
-                        SdnStatus::Available => {
-                            counts.sdn_zones.available += 1;
-                        }
-                        SdnStatus::Error => {
-                            counts.sdn_zones.error += 1;
-                        }
-                        SdnStatus::Unknown => {
-                            counts.sdn_zones.unknown += 1;
+                Resource::PveNetwork(r) => {
+                    if let PveNetworkResource::Zone(zone) = r {
+                        match zone.status() {
+                            SdnStatus::Available => {
+                                counts.sdn_zones.available += 1;
+                            }
+                            SdnStatus::Error => {
+                                counts.sdn_zones.error += 1;
+                            }
+                            SdnStatus::Unknown => {
+                                counts.sdn_zones.unknown += 1;
+                            }
                         }
                     }
                 }
@@ -1000,30 +1001,76 @@ pub(super) fn map_pve_storage(
     }
 }
 
-pub(super) fn map_pve_sdn(remote: &str, resource: ClusterResource) -> Option<PveSdnResource> {
+pub(super) fn map_pve_sdn(remote: &str, resource: ClusterResource) -> Option<PveNetworkResource> {
     match resource.ty {
         ClusterResourceType::Sdn => {
             let node = resource.node.unwrap_or_default();
 
-            Some(PveSdnResource::Zone(SdnZoneResource {
+            Some(PveNetworkResource::Zone(NetworkZoneResource {
                 id: format!("remote/{remote}/sdn/{}", &resource.id),
-                name: resource.sdn.unwrap_or_default(),
+                network: resource.sdn.unwrap_or_default(),
                 node,
+                // is empty in this format
+                zone_type: resource.zone_type.unwrap_or_default(),
                 status: SdnStatus::from_str(resource.status.unwrap_or_default().as_str())
                     .unwrap_or_default(),
+                legacy: true,
             }))
         }
         _ => None,
     }
 }
 
+pub(super) fn map_pve_network(
+    remote: &str,
+    resource: ClusterResource,
+) -> Option<PveNetworkResource> {
+    match resource.ty {
+        ClusterResourceType::Network => {
+            let Some(network_type) = resource.network_type else {
+                return None;
+            };
+
+            let id = format!("remote/{remote}/{}", &resource.id);
+            let node = resource.node.unwrap_or_default();
+            let network = resource.network.unwrap_or_default();
+            let status = SdnStatus::from_str(resource.status.unwrap_or_default().as_str())
+                .unwrap_or_default();
+
+            match network_type {
+                ClusterResourceNetworkType::Fabric => {
+                    Some(PveNetworkResource::Fabric(NetworkFabricResource {
+                        id,
+                        network,
+                        node,
+                        status,
+                        protocol: resource.protocol.unwrap_or_default(),
+                    }))
+                }
+                ClusterResourceNetworkType::Zone => {
+                    Some(PveNetworkResource::Zone(NetworkZoneResource {
+                        id,
+                        network,
+                        node,
+                        status,
+                        zone_type: resource.zone_type.unwrap_or_default(),
+                        legacy: false,
+                    }))
+                }
+            }
+        }
+        _ => None,
+    }
+}
+
 fn map_pve_resource(remote: &str, resource: ClusterResource) -> Option<Resource> {
     match resource.ty {
         ClusterResourceType::Node => map_pve_node(remote, resource).map(Resource::PveNode),
         ClusterResourceType::Lxc => map_pve_lxc(remote, resource).map(Resource::PveLxc),
         ClusterResourceType::Qemu => map_pve_qemu(remote, resource).map(Resource::PveQemu),
         ClusterResourceType::Storage => map_pve_storage(remote, resource).map(Resource::PveStorage),
-        ClusterResourceType::Sdn => map_pve_sdn(remote, resource).map(Resource::PveSdn),
+        ClusterResourceType::Sdn => map_pve_sdn(remote, resource).map(Resource::PveNetwork),
+        ClusterResourceType::Network => map_pve_network(remote, resource).map(Resource::PveNetwork),
         _ => None,
     }
 }
diff --git a/server/src/metric_collection/top_entities.rs b/server/src/metric_collection/top_entities.rs
index 73a3e63..5b58584 100644
--- a/server/src/metric_collection/top_entities.rs
+++ b/server/src/metric_collection/top_entities.rs
@@ -111,7 +111,7 @@ pub fn calculate_top(
                             }
                         }
                     }
-                    Resource::PveSdn(_) => {}
+                    Resource::PveNetwork(_) => {}
                     Resource::PbsDatastore(_) => {}
                 }
             }
-- 
2.47.3


_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


  parent reply	other threads:[~2025-11-07 14:39 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-11-07 14:40 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/6] Add support for " Stefan Hanreich
2025-11-07 14:40 ` [pdm-devel] [PATCH proxmox 1/3] pve-api-types: update /cluster/resources endpoint Stefan Hanreich
2025-11-07 14:40 ` [pdm-devel] [PATCH proxmox 2/3] pve-api-types: regenerate Stefan Hanreich
2025-11-07 14:40 ` [pdm-devel] [PATCH proxmox 3/3] pve-api-types: extend ClusterResourceNetworkType Stefan Hanreich
2025-11-07 14:40 ` Stefan Hanreich [this message]
2025-11-07 14:40 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/3] resources: api: add network type match category Stefan Hanreich
2025-11-07 14:40 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/3] ui: use new network resource type Stefan Hanreich

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251107144018.700695-5-s.hanreich@proxmox.com \
    --to=s.hanreich@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal