From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 261271FF16B for ; Fri, 7 Nov 2025 15:39:45 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id D8FD41509F; Fri, 7 Nov 2025 15:40:23 +0100 (CET) From: Stefan Hanreich To: pdm-devel@lists.proxmox.com Date: Fri, 7 Nov 2025 15:40:09 +0100 Message-ID: <20251107144018.700695-5-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251107144018.700695-1-s.hanreich@proxmox.com> References: <20251107144018.700695-1-s.hanreich@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.173 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Subject: [pdm-devel] [PATCH proxmox-datacenter-manager 1/3] resources: support new pve network resource type X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" 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 --- 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) -> 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 { } } -impl fmt::Display for PrintResource { +impl fmt::Display for PrintResource { 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 { +pub(super) fn map_pve_sdn(remote: &str, resource: ClusterResource) -> Option { 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 { + 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 { 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