From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 7CFB31FF17E for ; Thu, 13 Nov 2025 17:21:20 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3E95F22E85; Thu, 13 Nov 2025 17:21:21 +0100 (CET) From: Stefan Hanreich To: pdm-devel@lists.proxmox.com Date: Thu, 13 Nov 2025 17:20:51 +0100 Message-ID: <20251113162112.795835-4-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251113162112.795835-1-s.hanreich@proxmox.com> References: <20251113162112.795835-1-s.hanreich@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.174 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 RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. 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 v2 1/3] resources: views: 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 +- server/src/views/mod.rs | 2 +- 6 files changed, 159 insertions(+), 58 deletions(-) diff --git a/cli/client/src/resources.rs b/cli/client/src/resources.rs index bc3a98b..9b91a4b 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 9ea0be9..df2ebe0 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 cdbd90c..98239b0 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::metric_collection::top_entities; use crate::{connection, views}; @@ -482,18 +483,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; + } } } } @@ -1114,30 +1115,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 a91d586..e10c672 100644 --- a/server/src/metric_collection/top_entities.rs +++ b/server/src/metric_collection/top_entities.rs @@ -116,7 +116,7 @@ pub fn calculate_top( } } } - Resource::PveSdn(_) => {} + Resource::PveNetwork(_) => {} Resource::PbsDatastore(_) => {} } } diff --git a/server/src/views/mod.rs b/server/src/views/mod.rs index b5d79e0..80f8425 100644 --- a/server/src/views/mod.rs +++ b/server/src/views/mod.rs @@ -191,7 +191,7 @@ impl<'a> From<&'a Resource> for ResourceData<'a> { resource_id: value.global_id(), }, Resource::PveNode(_) - | Resource::PveSdn(_) + | Resource::PveNetwork(_) | Resource::PbsNode(_) | Resource::PbsDatastore(_) | Resource::PveStorage(_) => ResourceData { -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel