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 69ADD1FF191 for ; Tue, 9 Sep 2025 12:08:43 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 2ECCB5AA7; Tue, 9 Sep 2025 12:08:45 +0200 (CEST) From: Stefan Hanreich To: pdm-devel@lists.proxmox.com Date: Tue, 9 Sep 2025 12:08:29 +0200 Message-ID: <20250909100838.234778-3-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20250909100838.234778-1-s.hanreich@proxmox.com> References: <20250909100838.234778-1-s.hanreich@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.182 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 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [lib.rs, resource.rs, remote.rs, resources.rs, renderer.rs, tree.rs] Subject: [pdm-devel] [PATCH proxmox-datacenter-manager 1/5] pdm-api-types: add sdn cluster resource 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" SDN has its own cluster resource that reports the status of SDN zones. Add it to the existing Resource types, so it can be used in PDM. With the introduction of fabrics, another type of SDN resource will follow. In order to avoid creating a different Resource variant for each SDN entitiy, use a Resource::PveSdn variant instead, that itself is an enum. That allows for type-safety when introducing new SDN entities, without polluting the root Resource type. The status for localnetwork and other zones, if they are ok, diverges. localnetwork returns ok, while zones return available. In order to handle this, introduce a special SdnStatus that merges both of them into one variant: Available. That eases handling and prevents mistakes while maintaining backwards compatibility. Signed-off-by: Stefan Hanreich --- cli/client/src/resources.rs | 14 ++ lib/pdm-api-types/src/resource.rs | 144 ++++++++++++++++++- lib/proxmox-api-types | 2 +- server/src/api/resources.rs | 37 ++++- server/src/metric_collection/top_entities.rs | 1 + ui/src/lib.rs | 1 + ui/src/pve/remote.rs | 1 + ui/src/pve/tree.rs | 1 + ui/src/renderer.rs | 2 + 9 files changed, 200 insertions(+), 3 deletions(-) diff --git a/cli/client/src/resources.rs b/cli/client/src/resources.rs index cbc616a..dbf9f26 100644 --- a/cli/client/src/resources.rs +++ b/cli/client/src/resources.rs @@ -52,6 +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::PbsNode(r) => println!("{}", PrintResource(r)), Resource::PbsDatastore(r) => println!("{}", PrintResource(r)), } @@ -69,6 +70,7 @@ fn resource_order(item: &Resource) -> usize { Resource::PveStorage(_) => 1, Resource::PveLxc(_) => 2, Resource::PveQemu(_) => 3, + Resource::PveSdn(_) => 4, Resource::PbsNode(_) => 0, Resource::PbsDatastore(_) => 1, @@ -148,6 +150,18 @@ 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}", + name = self.0.name(), + status = self.0.status(), + node = self.0.node(), + ) + } +} + impl fmt::Display for PrintResource { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let resource::PbsNodeResource { diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs index fd2d49b..f274451 100644 --- a/lib/pdm-api-types/src/resource.rs +++ b/lib/pdm-api-types/src/resource.rs @@ -1,7 +1,9 @@ +use std::convert::Infallible; + use anyhow::{bail, Error}; use serde::{Deserialize, Serialize}; -use proxmox_schema::api; +use proxmox_schema::{api, ApiStringFormat, ApiType, EnumEntry, OneOfSchema, Schema, StringSchema}; use super::remotes::REMOTE_ID_SCHEMA; @@ -20,6 +22,7 @@ pub enum Resource { PveQemu(PveQemuResource), PveLxc(PveLxcResource), PveNode(PveNodeResource), + PveSdn(PveSdnResource), PbsNode(PbsNodeResource), PbsDatastore(PbsDatastoreResource), } @@ -33,6 +36,7 @@ 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::PbsNode(r) => format!("node/{}", r.name), Resource::PbsDatastore(r) => r.name.clone(), } @@ -46,6 +50,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::PbsNode(r) => r.id.as_str(), Resource::PbsDatastore(r) => r.id.as_str(), } @@ -59,6 +64,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::PbsNode(r) => r.name.as_str(), Resource::PbsDatastore(r) => r.name.as_str(), } @@ -69,6 +75,7 @@ impl Resource { Resource::PveStorage(_) => ResourceType::PveStorage, Resource::PveQemu(_) => ResourceType::PveQemu, Resource::PveLxc(_) => ResourceType::PveLxc, + Resource::PveSdn(PveSdnResource::Zone(_)) => ResourceType::PveSdnZone, Resource::PveNode(_) | Resource::PbsNode(_) => ResourceType::Node, Resource::PbsDatastore(_) => ResourceType::PbsDatastore, } @@ -80,6 +87,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::PbsNode(r) => { if r.uptime > 0 { "online" @@ -93,10 +101,12 @@ impl Resource { } #[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Type of a PDM resource. pub enum ResourceType { PveStorage, PveQemu, PveLxc, + PveSdnZone, PbsDatastore, Node, } @@ -108,6 +118,7 @@ impl ResourceType { ResourceType::PveStorage => "storage", ResourceType::PveQemu => "qemu", ResourceType::PveLxc => "lxc", + ResourceType::PveSdnZone => "sdn-zone", ResourceType::PbsDatastore => "datastore", ResourceType::Node => "node", } @@ -128,6 +139,7 @@ impl std::str::FromStr for ResourceType { "storage" => ResourceType::PveStorage, "qemu" => ResourceType::PveQemu, "lxc" => ResourceType::PveLxc, + "sdn-zone" => ResourceType::PveSdnZone, "datastore" => ResourceType::PbsDatastore, "node" => ResourceType::Node, _ => bail!("invalid resource type"), @@ -151,6 +163,7 @@ pub enum PveResource { Qemu(PveQemuResource), Lxc(PveLxcResource), Node(PveNodeResource), + Sdn(PveSdnResource), } #[api( @@ -297,6 +310,121 @@ pub struct PveStorageResource { pub status: String, } +#[api] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +/// SDN Zone +pub struct SdnZoneResource { + /// Resource ID + pub id: String, + /// Name of the resource + pub name: String, + /// Cluster node name + pub node: String, + /// SDN status (available / error) + pub status: SdnStatus, +} + +#[derive(Clone, Debug, Serialize, PartialEq, Copy, Default)] +#[serde(rename_all = "lowercase")] +/// the status of SDN entities +/// +/// On the PVE side we have Ok and Available, since SDN Zones have status available if they're ok, but the +/// localnetwork special zone has status ok. This enum merges both into the Available variant. +pub enum SdnStatus { + Available, + Error, + #[serde(other)] + #[default] + Unknown, +} + +impl std::str::FromStr for SdnStatus { + type Err = Infallible; + + fn from_str(value: &str) -> Result { + Ok(match value { + "ok" | "available" => Self::Available, + "error" => Self::Error, + _ => Self::Unknown, + }) + } +} + +proxmox_serde::forward_deserialize_to_from_str!(SdnStatus); +proxmox_serde::forward_display_to_serialize!(SdnStatus); + +impl SdnStatus { + pub fn as_str(&self) -> &'static str { + match self { + Self::Available => "available", + Self::Error => "error", + Self::Unknown => "unknown", + } + } +} + +impl ApiType for SdnStatus { + const API_SCHEMA: Schema = StringSchema::new("SDN status").schema(); +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(tag = "sdn_type", rename_all = "lowercase")] +/// SDN resource in PDM +pub enum PveSdnResource { + Zone(SdnZoneResource), +} + +impl ApiType for PveSdnResource { + const API_SCHEMA: Schema = OneOfSchema::new( + "PVE SDN resource", + &( + "sdn_type", + false, + &StringSchema::new("PVE SDN resource type") + .format(&ApiStringFormat::Enum(&[EnumEntry::new( + "zone", + "An SDN zone.", + )])) + .schema(), + ), + &[("zone", &SdnZoneResource::API_SCHEMA)], + ) + .schema(); +} + +impl PveSdnResource { + pub fn id(&self) -> &str { + match self { + Self::Zone(zone) => zone.id.as_str(), + } + } + + pub fn name(&self) -> &str { + match self { + Self::Zone(zone) => zone.name.as_str(), + } + } + + pub fn node(&self) -> &str { + match self { + Self::Zone(zone) => zone.node.as_str(), + } + } + + pub fn status(&self) -> SdnStatus { + match self { + Self::Zone(zone) => zone.status, + } + } + + pub fn sdn_type(&self) -> &'static str { + match self { + Self::Zone(_) => "sdn-zone", + } + } +} + #[api] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "kebab-case")] @@ -397,6 +525,18 @@ pub struct StorageStatusCount { pub unknown: u64, } +#[api] +#[derive(Default, Serialize, Deserialize, Clone, PartialEq)] +/// Amount of SDN zones in certain states +pub struct SdnZoneCount { + /// Amount of available / ok zones + pub available: u64, + /// Amount of erroneous sdn zones + pub error: u64, + /// Amount of sdn zones with an unknown status + pub unknown: u64, +} + #[api] #[derive(Default, Serialize, Deserialize, Clone, PartialEq)] /// Describes the status of seen resources @@ -413,6 +553,8 @@ pub struct ResourcesStatus { pub lxc: GuestStatusCount, /// Status of storage status pub storages: StorageStatusCount, + /// Status of storage status + pub sdn_zones: SdnZoneCount, /// Status of PBS Nodes pub pbs_nodes: NodeStatusCount, /// Status of PBS Datastores diff --git a/lib/proxmox-api-types b/lib/proxmox-api-types index cdb4dcf..973ed53 160000 --- a/lib/proxmox-api-types +++ b/lib/proxmox-api-types @@ -1 +1 @@ -Subproject commit cdb4dcfe0791c7960ccd0ead36ec2381c6e8e0be +Subproject commit 973ed53cd737f6b76351e66b061635678f104098 diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs index 98c4dea..736bfb9 100644 --- a/server/src/api/resources.rs +++ b/server/src/api/resources.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::str::FromStr; use std::sync::{LazyLock, RwLock}; use anyhow::{bail, format_err, Error}; @@ -9,7 +10,8 @@ use pbs_api_types::{DataStoreStatusListItem, NodeStatus}; use pdm_api_types::remotes::{Remote, RemoteType}; use pdm_api_types::resource::{ PbsDatastoreResource, PbsNodeResource, PveLxcResource, PveNodeResource, PveQemuResource, - PveStorageResource, RemoteResources, Resource, ResourcesStatus, TopEntities, + PveSdnResource, PveStorageResource, RemoteResources, Resource, ResourceType, ResourcesStatus, + SdnStatus, SdnZoneResource, TopEntities, }; use pdm_api_types::subscription::{ NodeSubscriptionInfo, RemoteSubscriptionState, RemoteSubscriptions, SubscriptionLevel, @@ -347,6 +349,21 @@ pub async fn get_status( "offline" => counts.pve_nodes.offline += 1, _ => counts.pve_nodes.unknown += 1, }, + Resource::PveSdn(r) => { + if 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; + } + } + } + } // FIXME better status for pbs/datastores Resource::PbsNode(_) => counts.pbs_nodes.online += 1, Resource::PbsDatastore(_) => counts.pbs_datastores.available += 1, @@ -836,12 +853,30 @@ pub(super) fn map_pve_storage( } } +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 { + id: format!("remote/{remote}/sdn/{}", &resource.id), + name: resource.sdn.unwrap_or_default(), + node, + status: SdnStatus::from_str(resource.status.unwrap_or_default().as_str()) + .unwrap_or_default(), + })) + } + _ => 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), _ => None, } } diff --git a/server/src/metric_collection/top_entities.rs b/server/src/metric_collection/top_entities.rs index 31e36c3..47fda24 100644 --- a/server/src/metric_collection/top_entities.rs +++ b/server/src/metric_collection/top_entities.rs @@ -100,6 +100,7 @@ pub fn calculate_top( } } } + Resource::PveSdn(_) => {} Resource::PbsNode(_) => {} Resource::PbsDatastore(_) => {} } diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 37e6458..5ffbff3 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -167,6 +167,7 @@ pub(crate) fn get_resource_node(resource: &Resource) -> Option<&str> { Resource::PveQemu(qemu) => Some(&qemu.node), Resource::PveLxc(lxc) => Some(&lxc.node), Resource::PveNode(node) => Some(&node.node), + Resource::PveSdn(sdn) => Some(&sdn.sdn), Resource::PbsNode(_) => None, Resource::PbsDatastore(_) => None, } diff --git a/ui/src/pve/remote.rs b/ui/src/pve/remote.rs index e9e3a84..5c53515 100644 --- a/ui/src/pve/remote.rs +++ b/ui/src/pve/remote.rs @@ -115,6 +115,7 @@ impl RemotePanelComp { _ => level = Some(""), } } + PveResource::Sdn(_) => {} } } // render, but this would be all better with some actual types... diff --git a/ui/src/pve/tree.rs b/ui/src/pve/tree.rs index 168e322..d161fc4 100644 --- a/ui/src/pve/tree.rs +++ b/ui/src/pve/tree.rs @@ -199,6 +199,7 @@ impl PveTreeComp { } node.append(PveTreeNode::Storage(storage.clone())); } + PveResource::Sdn(_) => {} } } if !self.loaded { diff --git a/ui/src/renderer.rs b/ui/src/renderer.rs index e74ab3d..5ebd9a3 100644 --- a/ui/src/renderer.rs +++ b/ui/src/renderer.rs @@ -17,6 +17,7 @@ pub fn render_resource_name(resource: &Resource, vmid_first: bool) -> String { Resource::PveQemu(qemu) => pve::utils::render_qemu_name(qemu, vmid_first), Resource::PveLxc(lxc) => pve::utils::render_lxc_name(lxc, vmid_first), Resource::PveNode(node) => node.node.clone(), + Resource::PveSdn(sdn) => sdn.sdn.clone(), Resource::PbsNode(node) => node.name.clone(), Resource::PbsDatastore(store) => store.name.clone(), } @@ -28,6 +29,7 @@ pub fn render_resource_icon(resource: &Resource) -> Fa { Resource::PveQemu(_) => "desktop", Resource::PveLxc(_) => "cube", Resource::PveNode(_) => "building", + Resource::PveSdn(_) => "fa-sdn", Resource::PbsNode(_) => "building-o", Resource::PbsDatastore(_) => "floppy-o", }; -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel