From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH proxmox-datacenter-manager v2 1/5] pdm-api-types: add sdn cluster resource
Date: Tue, 9 Sep 2025 17:54:16 +0200 [thread overview]
Message-ID: <20250909155423.526917-5-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20250909155423.526917-1-s.hanreich@proxmox.com>
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 <s.hanreich@proxmox.com>
---
cli/client/src/resources.rs | 14 ++
lib/pdm-api-types/src/resource.rs | 144 ++++++++++++++++++-
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 +
8 files changed, 199 insertions(+), 2 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<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::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<resource::PveNodeResource> {
}
}
+impl fmt::Display for PrintResource<resource::PveSdnResource> {
+ 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<resource::PbsNodeResource> {
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<Self, Infallible> {
+ 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/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<PveSdnResource> {
+ 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<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),
_ => 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..6370930 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.node()),
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..f137b68 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.name().to_string(),
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
next prev parent reply other threads:[~2025-09-09 15:54 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-09-09 15:54 [pdm-devel] [PATCH manager/proxmox{-api-types, -datacenter-manager} v2 0/8] Add SDN resources to dashboard + SDN zone overview tree Stefan Hanreich
2025-09-09 15:54 ` [pdm-devel] [PATCH pve-manager v2 1/1] cluster: resources: add sdn property to cluster resources schema Stefan Hanreich
2025-09-09 15:54 ` [pdm-devel] [PATCH proxmox-api-types v2 1/2] cluster: resource: add sdn property Stefan Hanreich
2025-09-09 15:54 ` [pdm-devel] [PATCH proxmox-api-types v2 2/2] regenerate Stefan Hanreich
2025-09-09 15:54 ` Stefan Hanreich [this message]
2025-09-09 15:54 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 2/5] server: api: add resource-type parameter to list_resources Stefan Hanreich
2025-09-09 15:54 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 3/5] ui: add sdn status report to dashboard Stefan Hanreich
2025-09-09 15:54 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 4/5] ui: images: add sdn icon Stefan Hanreich
2025-09-09 15:54 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 5/5] ui: sdn: add zone tree 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=20250909155423.526917-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.