* [pdm-devel] [PATCH proxmox 1/1] node-status: add node status crate
2025-10-28 16:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp 0/6] add node status panel to proxmox datacenter manager Shannon Sterz
@ 2025-10-28 16:44 ` Shannon Sterz
2025-10-28 16:44 ` [pdm-devel] [PATCH yew-comp 1/2] node info: extend NodeStatus enum to include NodeStatus from proxmox-rs Shannon Sterz
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Shannon Sterz @ 2025-10-28 16:44 UTC (permalink / raw)
To: pdm-devel
this includes api endpoints for querying api endpoints
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Cargo.toml | 1 +
proxmox-node-status/Cargo.toml | 37 +++++
proxmox-node-status/debian/changelog | 5 +
proxmox-node-status/debian/control | 65 ++++++++
proxmox-node-status/debian/copyright | 18 +++
proxmox-node-status/debian/debcargo.toml | 7 +
proxmox-node-status/src/api.rs | 184 +++++++++++++++++++++++
proxmox-node-status/src/lib.rs | 11 ++
proxmox-node-status/src/types.rs | 184 +++++++++++++++++++++++
9 files changed, 512 insertions(+)
create mode 100644 proxmox-node-status/Cargo.toml
create mode 100644 proxmox-node-status/debian/changelog
create mode 100644 proxmox-node-status/debian/control
create mode 100644 proxmox-node-status/debian/copyright
create mode 100644 proxmox-node-status/debian/debcargo.toml
create mode 100644 proxmox-node-status/src/api.rs
create mode 100644 proxmox-node-status/src/lib.rs
create mode 100644 proxmox-node-status/src/types.rs
diff --git a/Cargo.toml b/Cargo.toml
index 8091bf70..18c29afa 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@ members = [
"proxmox-metrics",
"proxmox-network-api",
"proxmox-network-types",
+ "proxmox-node-status",
"proxmox-notify",
"proxmox-openid",
"proxmox-product-config",
diff --git a/proxmox-node-status/Cargo.toml b/proxmox-node-status/Cargo.toml
new file mode 100644
index 00000000..2976d6a0
--- /dev/null
+++ b/proxmox-node-status/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "proxmox-node-status"
+description = "API implementation and types for querying a nodes status."
+version = "1.0.0"
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+anyhow = { workspace = true, optional = true }
+hex = { workspace = true, optional = true }
+nix = { workspace = true, optional = true }
+openssl = { workspace = true, optional = true }
+serde = { workspace = true, features = [ "derive" ] }
+serde_json.workspace = true
+tokio = { workspace = true, optional = true }
+
+proxmox-router = { workspace = true, optional = true }
+proxmox-schema = { workspace = true, features = [ "api-macro", "api-types" ] }
+proxmox-sys = { workspace = true, optional = true }
+
+[features]
+default = []
+api = [
+ "dep:anyhow",
+ "dep:hex",
+ "dep:nix",
+ "dep:openssl",
+ "dep:proxmox-router",
+ "dep:proxmox-sys",
+ "dep:tokio"
+]
diff --git a/proxmox-node-status/debian/changelog b/proxmox-node-status/debian/changelog
new file mode 100644
index 00000000..a7050f31
--- /dev/null
+++ b/proxmox-node-status/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-node-status (1.0.0-1) trixie; urgency=medium
+
+ * Initial packaging
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 22 Oct 2025 14:44:26 +0200
diff --git a/proxmox-node-status/debian/control b/proxmox-node-status/debian/control
new file mode 100644
index 00000000..48067be8
--- /dev/null
+++ b/proxmox-node-status/debian/control
@@ -0,0 +1,65 @@
+Source: rust-proxmox-node-status
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.82) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-proxmox-schema-5+api-macro-dev <!nocheck>,
+ librust-proxmox-schema-5+api-types-dev <!nocheck>,
+ librust-proxmox-schema-5+default-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>,
+ librust-serde-json-1+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-node-status
+
+Package: librust-proxmox-node-status-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-schema-5+api-macro-dev,
+ librust-proxmox-schema-5+api-types-dev,
+ librust-proxmox-schema-5+default-dev,
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-serde-json-1+default-dev
+Suggests:
+ librust-proxmox-node-status+api-dev (= ${binary:Version})
+Provides:
+ librust-proxmox-node-status+default-dev (= ${binary:Version}),
+ librust-proxmox-node-status-1-dev (= ${binary:Version}),
+ librust-proxmox-node-status-1+default-dev (= ${binary:Version}),
+ librust-proxmox-node-status-1.0-dev (= ${binary:Version}),
+ librust-proxmox-node-status-1.0+default-dev (= ${binary:Version}),
+ librust-proxmox-node-status-1.0.0-dev (= ${binary:Version}),
+ librust-proxmox-node-status-1.0.0+default-dev (= ${binary:Version})
+Description: API implementation and types for querying a nodes status - Rust source code
+ Source code for Debianized Rust crate "proxmox-node-status"
+
+Package: librust-proxmox-node-status+api-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-node-status-dev (= ${binary:Version}),
+ librust-anyhow-1+default-dev,
+ librust-hex-0.4+default-dev,
+ librust-nix-0.29+default-dev,
+ librust-openssl-0.10+default-dev,
+ librust-proxmox-router-3+default-dev (>= 3.2.2-~~),
+ librust-proxmox-sys-1+default-dev,
+ librust-tokio-1+default-dev (>= 1.6-~~)
+Provides:
+ librust-proxmox-node-status-1+api-dev (= ${binary:Version}),
+ librust-proxmox-node-status-1.0+api-dev (= ${binary:Version}),
+ librust-proxmox-node-status-1.0.0+api-dev (= ${binary:Version})
+Description: API implementation and types for querying a nodes status - feature "api"
+ This metapackage enables feature "api" for the Rust proxmox-node-status crate,
+ by pulling in any additional dependencies needed by that feature.
diff --git a/proxmox-node-status/debian/copyright b/proxmox-node-status/debian/copyright
new file mode 100644
index 00000000..d6e3c304
--- /dev/null
+++ b/proxmox-node-status/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-node-status/debian/debcargo.toml b/proxmox-node-status/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-node-status/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-node-status/src/api.rs b/proxmox-node-status/src/api.rs
new file mode 100644
index 00000000..93f6ae12
--- /dev/null
+++ b/proxmox-node-status/src/api.rs
@@ -0,0 +1,184 @@
+use std::path::{Path, PathBuf};
+use std::process::Command;
+use std::sync::OnceLock;
+
+use anyhow::{bail, format_err, Error};
+
+use proxmox_schema::api;
+use proxmox_schema::api_types::NODE_SCHEMA;
+use proxmox_sys::boot_mode;
+use proxmox_sys::linux::procfs;
+
+pub use crate::types::{
+ BootModeInformation, KernelVersionInformation, NodeCpuInformation, NodeInformation,
+ NodeMemoryCounters, NodePowerCommand, NodeStatus, NodeSwapCounters, StorageStatus,
+};
+
+static TLS_CERT_PATH: OnceLock<PathBuf> = OnceLock::new();
+
+pub fn init_node_status_api<P: AsRef<Path>>(cert_path: P) -> Result<(), Error> {
+ TLS_CERT_PATH
+ .set(cert_path.as_ref().to_owned())
+ .map_err(|_e| format_err!("cannot initialize acl tree config twice!"))
+}
+
+fn procfs_to_node_cpu_info(info: procfs::ProcFsCPUInfo) -> NodeCpuInformation {
+ NodeCpuInformation {
+ model: info.model,
+ sockets: info.sockets,
+ cpus: info.cpus,
+ }
+}
+
+fn boot_mode_to_info(bm: boot_mode::BootMode, sb: boot_mode::SecureBoot) -> BootModeInformation {
+ use boot_mode::BootMode;
+ use boot_mode::SecureBoot;
+
+ match (bm, sb) {
+ (BootMode::Efi, SecureBoot::Enabled) => BootModeInformation {
+ mode: crate::types::BootMode::Efi,
+ secureboot: true,
+ },
+ (BootMode::Efi, SecureBoot::Disabled) => BootModeInformation {
+ mode: crate::types::BootMode::Efi,
+ secureboot: false,
+ },
+ (BootMode::Bios, _) => BootModeInformation {
+ mode: crate::types::BootMode::LegacyBios,
+ secureboot: false,
+ },
+ }
+}
+
+fn certificate_fingerprint() -> Result<String, Error> {
+ let cert_path = TLS_CERT_PATH.get().ok_or_else(|| {
+ format_err!("certificate path needs to be set before calling node status endpoints")
+ })?;
+ let x509 = openssl::x509::X509::from_pem(&proxmox_sys::fs::file_get_contents(cert_path)?)?;
+ let fp = x509.digest(openssl::hash::MessageDigest::sha256())?;
+
+ Ok(hex::encode(fp)
+ .as_bytes()
+ .chunks(2)
+ .map(|v| std::str::from_utf8(v).unwrap())
+ .collect::<Vec<&str>>()
+ .join(":"))
+}
+
+#[api(
+ input: {
+ properties: {
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ },
+ },
+ returns: {
+ type: NodeStatus,
+ },
+)]
+/// Read node memory, CPU and (root) disk usage
+pub async fn get_status() -> Result<NodeStatus, Error> {
+ let meminfo: procfs::ProcFsMemInfo = procfs::read_meminfo()?;
+ let memory = NodeMemoryCounters {
+ total: meminfo.memtotal,
+ used: meminfo.memused,
+ free: meminfo.memfree,
+ };
+
+ let swap = NodeSwapCounters {
+ total: meminfo.swaptotal,
+ used: meminfo.swapused,
+ free: meminfo.swapfree,
+ };
+
+ let kstat: procfs::ProcFsStat = procfs::read_proc_stat()?;
+ let cpu = kstat.cpu;
+ let wait = kstat.iowait_percent;
+
+ let loadavg = procfs::Loadavg::read()?;
+ let loadavg = [loadavg.one(), loadavg.five(), loadavg.fifteen()];
+
+ let cpuinfo = procfs::read_cpuinfo()?;
+ let cpuinfo = procfs_to_node_cpu_info(cpuinfo);
+
+ let uname = nix::sys::utsname::uname()?;
+ let kernel_version = KernelVersionInformation::from_uname_parts(
+ uname.sysname(),
+ uname.release(),
+ uname.version(),
+ uname.machine(),
+ );
+
+ let disk = tokio::task::spawn_blocking(move || proxmox_sys::fs::fs_info(c"/"))
+ .await
+ .map_err(|err| format_err!("error waiting for fs_info call: {err}"))??;
+
+ let boot_info = boot_mode_to_info(boot_mode::BootMode::query(), boot_mode::SecureBoot::query());
+
+ Ok(NodeStatus {
+ memory,
+ swap,
+ root: StorageStatus {
+ total: disk.total,
+ used: disk.used,
+ avail: disk.available,
+ },
+ uptime: procfs::read_proc_uptime()?.0 as u64,
+ loadavg,
+ kversion: kernel_version.get_legacy(),
+ current_kernel: kernel_version,
+ cpuinfo,
+ cpu,
+ wait,
+ info: NodeInformation {
+ fingerprint: certificate_fingerprint()?,
+ },
+ boot_info,
+ })
+}
+
+#[api(
+ protected: true,
+ input: {
+ properties: {
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ command: {
+ type: NodePowerCommand,
+ },
+ }
+ },
+)]
+/// Reboot or shutdown the node.
+pub fn reboot_or_shutdown(command: NodePowerCommand) -> Result<(), Error> {
+ let systemctl_command = match command {
+ NodePowerCommand::Reboot => "reboot",
+ NodePowerCommand::Shutdown => "poweroff",
+ };
+
+ let output = Command::new("systemctl")
+ .arg(systemctl_command)
+ .output()
+ .map_err(|err| format_err!("failed to execute systemctl - {err}"))?;
+
+ if !output.status.success() {
+ match output.status.code() {
+ Some(code) => {
+ let msg = String::from_utf8(output.stderr)
+ .map(|m| {
+ if m.is_empty() {
+ String::from("no error message")
+ } else {
+ m
+ }
+ })
+ .unwrap_or_else(|_| String::from("non utf8 error message (suppressed)"));
+ bail!("command failed with status code: {code} - {msg}");
+ }
+ None => bail!("systemctl terminated by signal"),
+ }
+ }
+ Ok(())
+}
diff --git a/proxmox-node-status/src/lib.rs b/proxmox-node-status/src/lib.rs
new file mode 100644
index 00000000..2372b569
--- /dev/null
+++ b/proxmox-node-status/src/lib.rs
@@ -0,0 +1,11 @@
+
+#[cfg(feature = "api")]
+mod api;
+#[cfg(feature = "api")]
+pub use crate::api::{init_node_status_api, API_METHOD_GET_STATUS, API_METHOD_REBOOT_OR_SHUTDOWN};
+
+mod types;
+pub use crate::types::{
+ BootMode, BootModeInformation, KernelVersionInformation, NodeCpuInformation, NodeInformation,
+ NodeMemoryCounters, NodePowerCommand, NodeStatus, NodeSwapCounters, StorageStatus,
+};
diff --git a/proxmox-node-status/src/types.rs b/proxmox-node-status/src/types.rs
new file mode 100644
index 00000000..cc0ba424
--- /dev/null
+++ b/proxmox-node-status/src/types.rs
@@ -0,0 +1,184 @@
+use std::ffi::OsStr;
+
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::api;
+
+#[api]
+#[derive(Serialize, Deserialize, Copy, Clone)]
+#[serde(rename_all = "kebab-case")]
+/// The possible BootModes
+pub enum BootMode {
+ /// The BootMode is EFI/UEFI
+ Efi,
+ /// The BootMode is Legacy BIOS
+ LegacyBios,
+}
+
+#[api]
+#[derive(Serialize, Deserialize, Clone)]
+#[serde(rename_all = "lowercase")]
+/// Holds the Bootmodes
+pub struct BootModeInformation {
+ /// The BootMode, either Efi or Bios
+ pub mode: BootMode,
+ /// SecureBoot status
+ pub secureboot: bool,
+}
+
+#[api]
+#[derive(Serialize, Deserialize, Default)]
+#[serde(rename_all = "lowercase")]
+/// The current kernel version (output of `uname`)
+pub struct KernelVersionInformation {
+ /// The systemname/nodename
+ pub sysname: String,
+ /// The kernel release number
+ pub release: String,
+ /// The kernel version
+ pub version: String,
+ /// The machine architecture
+ pub machine: String,
+}
+
+impl KernelVersionInformation {
+ pub fn from_uname_parts(
+ sysname: &OsStr,
+ release: &OsStr,
+ version: &OsStr,
+ machine: &OsStr,
+ ) -> Self {
+ KernelVersionInformation {
+ sysname: sysname.to_str().map(String::from).unwrap_or_default(),
+ release: release.to_str().map(String::from).unwrap_or_default(),
+ version: version.to_str().map(String::from).unwrap_or_default(),
+ machine: machine.to_str().map(String::from).unwrap_or_default(),
+ }
+ }
+
+ pub fn get_legacy(&self) -> String {
+ format!("{} {} {}", self.sysname, self.release, self.version)
+ }
+}
+
+#[api]
+#[derive(Serialize, Deserialize, Default)]
+#[serde(rename_all = "kebab-case")]
+/// Information about the CPU
+pub struct NodeCpuInformation {
+ /// The CPU model
+ pub model: String,
+ /// The number of CPU sockets
+ pub sockets: usize,
+ /// The number of CPU cores (incl. threads)
+ pub cpus: usize,
+}
+
+#[api]
+#[derive(Serialize, Deserialize, Default)]
+#[serde(rename_all = "kebab-case")]
+/// Contains general node information such as the fingerprint`
+pub struct NodeInformation {
+ /// The SSL Fingerprint
+ pub fingerprint: String,
+}
+
+#[api]
+#[derive(Serialize, Deserialize, Default)]
+#[serde(rename_all = "kebab-case")]
+/// Node memory usage counters
+pub struct NodeMemoryCounters {
+ /// Total memory
+ pub total: u64,
+ /// Used memory
+ pub used: u64,
+ /// Free memory
+ pub free: u64,
+}
+
+#[api(
+ properties: {
+ memory: {
+ type: NodeMemoryCounters,
+ },
+ root: {
+ type: StorageStatus,
+ },
+ swap: {
+ type: NodeSwapCounters,
+ },
+ loadavg: {
+ type: Array,
+ items: {
+ type: Number,
+ description: "the load",
+ }
+ },
+ cpuinfo: {
+ type: NodeCpuInformation,
+ },
+ info: {
+ type: NodeInformation,
+ }
+ },
+)]
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+/// The Node status
+pub struct NodeStatus {
+ pub memory: NodeMemoryCounters,
+ pub root: StorageStatus,
+ pub swap: NodeSwapCounters,
+ /// The current uptime of the server.
+ pub uptime: u64,
+ /// Load for 1, 5 and 15 minutes.
+ pub loadavg: [f64; 3],
+ /// The current kernel version (NEW struct type).
+ pub current_kernel: KernelVersionInformation,
+ /// The current kernel version (LEGACY string type).
+ pub kversion: String,
+ /// Total CPU usage since last query.
+ pub cpu: f64,
+ /// Total IO wait since last query.
+ pub wait: f64,
+ pub cpuinfo: NodeCpuInformation,
+ pub info: NodeInformation,
+ /// Current boot mode
+ pub boot_info: BootModeInformation,
+}
+
+#[api]
+#[derive(Serialize, Deserialize, Default)]
+#[serde(rename_all = "kebab-case")]
+/// Node swap usage counters
+pub struct NodeSwapCounters {
+ /// Total swap
+ pub total: u64,
+ /// Used swap
+ pub used: u64,
+ /// Free swap
+ pub free: u64,
+}
+
+#[api()]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+/// Node Power command type.
+pub enum NodePowerCommand {
+ /// Restart the server
+ Reboot,
+ /// Shutdown the server
+ Shutdown,
+}
+
+#[api()]
+#[derive(Default, Serialize, Deserialize)]
+/// Storage space usage information.
+pub struct StorageStatus {
+ /// Total space (bytes).
+ pub total: u64,
+ /// Used space (bytes).
+ pub used: u64,
+ /// Available space (bytes).
+ pub avail: u64,
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 7+ messages in thread* [pdm-devel] [PATCH yew-comp 1/2] node info: extend NodeStatus enum to include NodeStatus from proxmox-rs
2025-10-28 16:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp 0/6] add node status panel to proxmox datacenter manager Shannon Sterz
2025-10-28 16:44 ` [pdm-devel] [PATCH proxmox 1/1] node-status: add node status crate Shannon Sterz
@ 2025-10-28 16:44 ` Shannon Sterz
2025-10-28 16:44 ` [pdm-devel] [PATCH yew-comp 2/2] node status panel: add a panel that show the current status of a node Shannon Sterz
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Shannon Sterz @ 2025-10-28 16:44 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
src/node_info.rs | 38 ++++++++++++++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/src/node_info.rs b/src/node_info.rs
index 17ba6cd..5604787 100644
--- a/src/node_info.rs
+++ b/src/node_info.rs
@@ -1,4 +1,5 @@
use proxmox_human_byte::HumanByte;
+use proxmox_node_status::BootMode;
use pwt::{prelude::*, widget::Container};
use crate::{MeterLabel, StatusRow};
@@ -7,6 +8,7 @@ use crate::{MeterLabel, StatusRow};
pub enum NodeStatus<'a> {
Pve(&'a pve_api_types::NodeStatus),
Pbs(&'a pbs_api_types::NodeStatus),
+ Common(&'a proxmox_node_status::NodeStatus),
}
impl<'a> From<&'a pve_api_types::NodeStatus> for NodeStatus<'a> {
@@ -29,6 +31,7 @@ pub fn node_info(data: Option<NodeStatus>) -> Container {
let (cpu, cpus_total) = match data {
Some(NodeStatus::Pve(node_status)) => (node_status.cpu, node_status.cpuinfo.cpus as u64),
Some(NodeStatus::Pbs(node_status)) => (node_status.cpu, node_status.cpuinfo.cpus as u64),
+ Some(NodeStatus::Common(node_status)) => (node_status.cpu, node_status.cpuinfo.cpus as u64),
None => (0.0, 1),
};
@@ -39,6 +42,7 @@ pub fn node_info(data: Option<NodeStatus>) -> Container {
.and_then(|wait| wait.as_f64())
.unwrap_or_default(),
Some(NodeStatus::Pbs(node_status)) => node_status.wait,
+ Some(NodeStatus::Common(node_status)) => node_status.wait,
None => 0.0,
};
@@ -48,6 +52,9 @@ pub fn node_info(data: Option<NodeStatus>) -> Container {
node_status.memory.total as u64,
),
Some(NodeStatus::Pbs(node_status)) => (node_status.memory.used, node_status.memory.total),
+ Some(NodeStatus::Common(node_status)) => {
+ (node_status.memory.used, node_status.memory.total)
+ }
None => (0, 1),
};
@@ -57,6 +64,10 @@ pub fn node_info(data: Option<NodeStatus>) -> Container {
"{:.2} {:.2} {:.2}",
node_status.loadavg[0], node_status.loadavg[1], node_status.loadavg[2]
),
+ Some(NodeStatus::Common(node_status)) => format!(
+ "{:.2} {:.2} {:.2}",
+ node_status.loadavg[0], node_status.loadavg[1], node_status.loadavg[2]
+ ),
None => tr!("N/A"),
};
@@ -66,6 +77,7 @@ pub fn node_info(data: Option<NodeStatus>) -> Container {
node_status.rootfs.total as u64,
),
Some(NodeStatus::Pbs(node_status)) => (node_status.root.used, node_status.root.total),
+ Some(NodeStatus::Common(node_status)) => (node_status.root.used, node_status.root.total),
None => (0, 1),
};
@@ -90,6 +102,7 @@ pub fn node_info(data: Option<NodeStatus>) -> Container {
}
}
Some(NodeStatus::Pbs(node_status)) => (node_status.swap.used, node_status.swap.total),
+ Some(NodeStatus::Common(node_status)) => (node_status.swap.used, node_status.swap.total),
None => (0, 1),
};
@@ -102,6 +115,10 @@ pub fn node_info(data: Option<NodeStatus>) -> Container {
node_status.cpuinfo.model.clone(),
node_status.cpuinfo.sockets as u64,
),
+ Some(NodeStatus::Common(node_status)) => (
+ node_status.cpuinfo.model.clone(),
+ node_status.cpuinfo.sockets as u64,
+ ),
None => (String::new(), 1),
};
@@ -121,9 +138,20 @@ pub fn node_info(data: Option<NodeStatus>) -> Container {
node_status.current_kernel.release.clone(),
node_status.current_kernel.version.clone(),
),
+ Some(NodeStatus::Common(node_status)) => (
+ node_status.current_kernel.sysname.clone(),
+ node_status.current_kernel.release.clone(),
+ node_status.current_kernel.version.clone(),
+ ),
None => (String::new(), String::new(), String::new()),
};
+ let boot_mode = if let Some(NodeStatus::Common(node_status)) = data {
+ Some(&node_status.boot_info)
+ } else {
+ None
+ };
+
Container::new()
.class("pwt-d-grid pwt-gap-2 pwt-align-items-center")
.style("grid-template-columns", "1fr 20px 1fr")
@@ -221,4 +249,14 @@ pub fn node_info(data: Option<NodeStatus>) -> Container {
.style("grid-column", "1/-1")
.status(format!("{} {} {}", k_sysname, k_release, k_version)),
)
+ .with_optional_child(boot_mode.map(|m| {
+ let mode = match m.mode {
+ BootMode::Efi => tr!("Legacy BIOS"),
+ BootMode::LegacyBios if m.secureboot => tr!("UEFI (Secure Boot Enabled)"),
+ BootMode::LegacyBios => tr!("UEFI"),
+ };
+ StatusRow::new(tr!("Boot Mode"))
+ .style("grid-column", "1/-1")
+ .status(mode)
+ }))
}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 7+ messages in thread* [pdm-devel] [PATCH yew-comp 2/2] node status panel: add a panel that show the current status of a node
2025-10-28 16:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp 0/6] add node status panel to proxmox datacenter manager Shannon Sterz
2025-10-28 16:44 ` [pdm-devel] [PATCH proxmox 1/1] node-status: add node status crate Shannon Sterz
2025-10-28 16:44 ` [pdm-devel] [PATCH yew-comp 1/2] node info: extend NodeStatus enum to include NodeStatus from proxmox-rs Shannon Sterz
@ 2025-10-28 16:44 ` Shannon Sterz
2025-10-28 16:44 ` [pdm-devel] [PATCH datacenter-manager 1/3] api-types/api: add endpoints for querying the node's status Shannon Sterz
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Shannon Sterz @ 2025-10-28 16:44 UTC (permalink / raw)
To: pdm-devel
it also allows shutting down or reloading the node.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Cargo.toml | 1 +
src/lib.rs | 3 +
src/node_status_panel.rs | 244 +++++++++++++++++++++++++++++++++++++++
3 files changed, 248 insertions(+)
create mode 100644 src/node_status_panel.rs
diff --git a/Cargo.toml b/Cargo.toml
index 235aaea..12421b0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -84,6 +84,7 @@ proxmox-apt-api-types = { version = "2.0", optional = true }
proxmox-access-control = "1.1"
proxmox-dns-api = { version = "1", optional = true }
proxmox-network-api = { version = "1", optional = true }
+proxmox-node-status = { version = "1", features = [] }
pve-api-types = "8"
pbs-api-types = "1"
diff --git a/src/lib.rs b/src/lib.rs
index 3a9e32b..e097b05 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -91,6 +91,9 @@ pub use loadable_component::{
mod node_info;
pub use node_info::{node_info, NodeStatus};
+mod node_status_panel;
+pub use node_status_panel::NodeStatusPanel;
+
mod notes_view;
pub use notes_view::{NotesView, NotesWithDigest, ProxmoxNotesView};
diff --git a/src/node_status_panel.rs b/src/node_status_panel.rs
new file mode 100644
index 0000000..da21feb
--- /dev/null
+++ b/src/node_status_panel.rs
@@ -0,0 +1,244 @@
+use std::future::Future;
+use std::rc::Rc;
+
+use anyhow::Error;
+use html::IntoPropValue;
+use pwt::css::{AlignItems, ColorScheme};
+use pwt::widget::form::DisplayField;
+use yew::virtual_dom::{VComp, VNode};
+
+use pwt::prelude::*;
+use pwt::widget::{error_message, Fa, Panel, Row, Tooltip};
+use pwt::widget::{Button, Dialog};
+use pwt_macros::builder;
+
+use proxmox_node_status::{NodePowerCommand, NodeStatus};
+
+use crate::utils::copy_text_to_clipboard;
+use crate::{
+ http_get, http_post, node_info, ConfirmButton, LoadableComponent, LoadableComponentContext,
+ LoadableComponentMaster,
+};
+
+#[derive(Properties, Clone, PartialEq)]
+#[builder]
+pub struct NodeStatusPanel {
+ /// URL path to load the node's status from.
+ #[builder(IntoPropValue, into_prop_value)]
+ #[prop_or_default]
+ status_base_url: Option<AttrValue>,
+}
+
+impl NodeStatusPanel {
+ pub fn new() -> Self {
+ yew::props!(Self {})
+ }
+}
+
+impl Default for NodeStatusPanel {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+enum Msg {
+ Error(Error),
+ Loaded(Rc<NodeStatus>),
+ RebootOrShutdown(NodePowerCommand),
+ Reload,
+}
+
+#[derive(PartialEq)]
+enum ViewState {
+ FingerprintDialog,
+}
+
+struct ProxmoxNodeStatusPanel {
+ node_status: Option<Rc<NodeStatus>>,
+ error: Option<Error>,
+}
+
+impl ProxmoxNodeStatusPanel {
+ fn change_power_state(&self, ctx: &LoadableComponentContext<Self>, command: NodePowerCommand) {
+ let Some(url) = ctx.props().status_base_url.clone() else {
+ return;
+ };
+ let link = ctx.link().clone();
+
+ ctx.link().spawn(async move {
+ let data = Some(serde_json::json!({
+ "command": command,
+ }));
+
+ match http_post(url.as_str(), data).await {
+ Ok(()) => link.send_message(Msg::Reload),
+ Err(err) => link.send_message(Msg::Error(err)),
+ }
+ });
+ }
+
+ fn fingerprint_dialog(
+ &self,
+ ctx: &LoadableComponentContext<Self>,
+ fingerprint: &str,
+ ) -> Dialog {
+ let link = ctx.link();
+ let link_button = ctx.link();
+ let fingerprint = fingerprint.to_owned();
+
+ Dialog::new(tr!("Fingerprint"))
+ .resizable(true)
+ .min_width(500)
+ .on_close(move |_| link.change_view(None))
+ .with_child(
+ Row::new()
+ .gap(2)
+ .margin_start(2)
+ .margin_end(2)
+ .with_child(
+ DisplayField::new()
+ .class(pwt::css::FlexFit)
+ .value(fingerprint.clone())
+ .border(true),
+ )
+ .with_child(
+ Tooltip::new(
+ Button::new_icon("fa fa-clipboard")
+ .class(ColorScheme::Primary)
+ .on_activate(move |_| copy_text_to_clipboard(&fingerprint)),
+ )
+ .tip(tr!("Copy token secret to clipboard.")),
+ ),
+ )
+ .with_child(
+ Row::new()
+ .padding(2)
+ .with_flex_spacer()
+ .with_child(
+ Button::new(tr!("OK")).on_activate(move |_| link_button.change_view(None)),
+ )
+ .with_flex_spacer(),
+ )
+ }
+}
+
+impl LoadableComponent for ProxmoxNodeStatusPanel {
+ type Message = Msg;
+ type ViewState = ViewState;
+ type Properties = NodeStatusPanel;
+
+ fn create(ctx: &crate::LoadableComponentContext<Self>) -> Self {
+ ctx.link().repeated_load(5000);
+
+ Self {
+ node_status: None,
+ error: None,
+ }
+ }
+
+ fn load(
+ &self,
+ ctx: &crate::LoadableComponentContext<Self>,
+ ) -> std::pin::Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
+ let url = ctx.props().status_base_url.clone();
+ let link = ctx.link().clone();
+
+ Box::pin(async move {
+ if let Some(url) = url {
+ match http_get(url.as_str(), None).await {
+ Ok(res) => link.send_message(Msg::Loaded(Rc::new(res))),
+ Err(err) => link.send_message(Msg::Error(err)),
+ }
+ }
+ Ok(())
+ })
+ }
+
+ fn update(&mut self, ctx: &crate::LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+ match msg {
+ Msg::Error(err) => {
+ self.error = Some(err);
+ true
+ }
+ Msg::Loaded(status) => {
+ self.node_status = Some(status);
+ self.error = None;
+ true
+ }
+ Msg::RebootOrShutdown(command) => {
+ self.change_power_state(ctx, command);
+ false
+ }
+ Msg::Reload => true,
+ }
+ }
+
+ fn dialog_view(
+ &self,
+ ctx: &LoadableComponentContext<Self>,
+ view_state: &Self::ViewState,
+ ) -> Option<Html> {
+ if view_state == &ViewState::FingerprintDialog {
+ if let Some(ref node_status) = self.node_status {
+ return Some(
+ self.fingerprint_dialog(&ctx, &node_status.info.fingerprint)
+ .into(),
+ );
+ }
+ }
+ None
+ }
+
+ fn main_view(&self, ctx: &crate::LoadableComponentContext<Self>) -> Html {
+ let status = self
+ .node_status
+ .as_ref()
+ .map(|r| crate::NodeStatus::Common(r));
+
+ Panel::new()
+ .title(
+ Row::new()
+ .class(AlignItems::Center)
+ .gap(2)
+ .with_child(Fa::new("book"))
+ .with_child(tr!("Node Status"))
+ .into_html(),
+ )
+ .with_tool(
+ ConfirmButton::new(tr!("Reboot"))
+ .confirm_message(tr!("Are you sure you want to reboot the node?"))
+ .on_activate(
+ ctx.link()
+ .callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Reboot)),
+ )
+ .icon_class("fa fa-undo"),
+ )
+ .with_tool(
+ ConfirmButton::new(tr!("Shutdown"))
+ .confirm_message(tr!("Are you sure you want to shut down the node?"))
+ .on_activate(
+ ctx.link()
+ .callback(|_| Msg::RebootOrShutdown(NodePowerCommand::Shutdown)),
+ )
+ .icon_class("fa fa-power-off"),
+ )
+ .with_tool(
+ Button::new(tr!("Show Fingerprint"))
+ .icon_class("fa fa-hashtag")
+ .class(ColorScheme::Primary)
+ .on_activate(
+ ctx.link()
+ .change_view_callback(|_| ViewState::FingerprintDialog),
+ ),
+ )
+ .with_child(node_info(status))
+ .with_optional_child(self.error.as_ref().map(|e| error_message(&e.to_string())))
+ .into()
+ }
+}
+
+impl From<NodeStatusPanel> for VNode {
+ fn from(value: NodeStatusPanel) -> Self {
+ VComp::new::<LoadableComponentMaster<ProxmoxNodeStatusPanel>>(Rc::new(value), None).into()
+ }
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 7+ messages in thread* [pdm-devel] [PATCH datacenter-manager 1/3] api-types/api: add endpoints for querying the node's status
2025-10-28 16:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp 0/6] add node status panel to proxmox datacenter manager Shannon Sterz
` (2 preceding siblings ...)
2025-10-28 16:44 ` [pdm-devel] [PATCH yew-comp 2/2] node status panel: add a panel that show the current status of a node Shannon Sterz
@ 2025-10-28 16:44 ` Shannon Sterz
2025-10-28 16:44 ` [pdm-devel] [RFC PATCH datacenter-manager 2/3] ui: add NodeStatusPanel to the administration menu Shannon Sterz
2025-10-28 16:44 ` [pdm-devel] [RFC PATCH datacenter-manager 3/3] nodes: remove unnecessary rustfmt::skip macro Shannon Sterz
5 siblings, 0 replies; 7+ messages in thread
From: Shannon Sterz @ 2025-10-28 16:44 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
Cargo.toml | 2 ++
lib/pdm-api-types/src/acl.rs | 2 ++
server/Cargo.toml | 1 +
server/src/api/nodes/mod.rs | 2 ++
server/src/api/nodes/status.rs | 18 ++++++++++++++++++
server/src/bin/proxmox-datacenter-api/main.rs | 2 ++
6 files changed, 27 insertions(+)
create mode 100644 server/src/api/nodes/status.rs
diff --git a/Cargo.toml b/Cargo.toml
index 49c7583..3252ccb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -78,6 +78,7 @@ proxmox-time-api = "1"
proxmox-network-api = "1"
proxmox-syslog-api = "1"
proxmox-acme-api = "1"
+proxmox-node-status = "1"
# API types for PVE (and later PMG?)
pve-api-types = "8.0.5"
@@ -163,6 +164,7 @@ zstd = { version = "0.13" }
# proxmox-log = { path = "../proxmox/proxmox-log" }
# proxmox-metrics = { path = "../proxmox/proxmox-metrics" }
# proxmox-network-api = { path = "../proxmox/proxmox-network-api" }
+# proxmox-node-status = { path = "../proxmox/proxmox-node-status" }
# proxmox-notify = { path = "../proxmox/proxmox-notify" }
# proxmox-openid = { path = "../proxmox/proxmox-openid" }
# proxmox-product-config = { path = "../proxmox/proxmox-product-config" }
diff --git a/lib/pdm-api-types/src/acl.rs b/lib/pdm-api-types/src/acl.rs
index 9e69c2f..5592102 100644
--- a/lib/pdm-api-types/src/acl.rs
+++ b/lib/pdm-api-types/src/acl.rs
@@ -26,6 +26,8 @@ constnamedbitmap! {
PRIV_SYS_MODIFY("System.Modify");
/// `Sys.Console` allows access to the system's console
PRIV_SYS_CONSOLE("Sys.Console");
+ /// `Sys.PowerManagement` allows powering off or rebooting the system.
+ PRIV_SYS_POWER_MANAGEMENT("Sys.PowerManagement");
/// `Resource.Audit` allows auditing guests, storages and other resources.
PRIV_RESOURCE_AUDIT("Resource.Audit");
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 94420b4..88e3802 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -72,6 +72,7 @@ proxmox-time-api = { workspace = true, features = [ "impl" ] }
proxmox-network-api = { workspace = true, features = [ "impl" ] }
proxmox-syslog-api = { workspace = true, features = [ "impl" ] }
proxmox-acme-api = { workspace = true, features = [ "impl" ] }
+proxmox-node-status = { workspace = true }
pdm-api-types.workspace = true
pdm-buildcfg.workspace = true
diff --git a/server/src/api/nodes/mod.rs b/server/src/api/nodes/mod.rs
index 6f30ba7..f70fcaf 100644
--- a/server/src/api/nodes/mod.rs
+++ b/server/src/api/nodes/mod.rs
@@ -10,6 +10,7 @@ pub mod dns;
pub mod journal;
pub mod network;
pub mod rrddata;
+pub mod status;
pub mod syslog;
pub mod tasks;
pub mod termproxy;
@@ -45,6 +46,7 @@ pub const SUBDIRS: SubdirMap = &sorted!([
("journal", &journal::ROUTER),
("network", &network::ROUTER),
("rrdata", &rrddata::ROUTER),
+ ("status", &status::ROUTER),
("syslog", &syslog::ROUTER),
("tasks", &tasks::ROUTER),
("termproxy", &termproxy::ROUTER),
diff --git a/server/src/api/nodes/status.rs b/server/src/api/nodes/status.rs
new file mode 100644
index 0000000..b3bbed5
--- /dev/null
+++ b/server/src/api/nodes/status.rs
@@ -0,0 +1,18 @@
+use pdm_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_POWER_MANAGEMENT};
+use proxmox_router::{ApiMethod, Permission, Router};
+
+const API_METHOD_GET_STATUS_WITH_ACCESS: ApiMethod = proxmox_node_status::API_METHOD_GET_STATUS
+ .access(
+ None,
+ &Permission::Privilege(&["system", "status"], PRIV_SYS_AUDIT, false),
+ );
+
+const API_METHOD_REBOOT_OR_SHUTDOWN_WITH_ACCESS: ApiMethod =
+ proxmox_node_status::API_METHOD_REBOOT_OR_SHUTDOWN.access(
+ None,
+ &Permission::Privilege(&["system", "status"], PRIV_SYS_POWER_MANAGEMENT, false),
+ );
+
+pub const ROUTER: Router = Router::new()
+ .get(&API_METHOD_GET_STATUS_WITH_ACCESS)
+ .post(&API_METHOD_REBOOT_OR_SHUTDOWN_WITH_ACCESS);
diff --git a/server/src/bin/proxmox-datacenter-api/main.rs b/server/src/bin/proxmox-datacenter-api/main.rs
index 420a3b4..860612c 100644
--- a/server/src/bin/proxmox-datacenter-api/main.rs
+++ b/server/src/bin/proxmox-datacenter-api/main.rs
@@ -391,6 +391,8 @@ fn make_tls_acceptor() -> Result<SslAcceptor, Error> {
let key_path = configdir!("/auth/api.key");
let cert_path = configdir!("/auth/api.pem");
+ proxmox_node_status::init_node_status_api(cert_path);
+
proxmox_rest_server::connection::TlsAcceptorBuilder::new()
.certificate_paths_pem(key_path, cert_path)
.build()
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 7+ messages in thread* [pdm-devel] [RFC PATCH datacenter-manager 2/3] ui: add NodeStatusPanel to the administration menu
2025-10-28 16:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp 0/6] add node status panel to proxmox datacenter manager Shannon Sterz
` (3 preceding siblings ...)
2025-10-28 16:44 ` [pdm-devel] [PATCH datacenter-manager 1/3] api-types/api: add endpoints for querying the node's status Shannon Sterz
@ 2025-10-28 16:44 ` Shannon Sterz
2025-10-28 16:44 ` [pdm-devel] [RFC PATCH datacenter-manager 3/3] nodes: remove unnecessary rustfmt::skip macro Shannon Sterz
5 siblings, 0 replies; 7+ messages in thread
From: Shannon Sterz @ 2025-10-28 16:44 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
might make sense to implement this as a widget for the new views feature
instead of using the panel here.
ui/src/administration/mod.rs | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/ui/src/administration/mod.rs b/ui/src/administration/mod.rs
index 38411a9..f245396 100644
--- a/ui/src/administration/mod.rs
+++ b/ui/src/administration/mod.rs
@@ -16,7 +16,9 @@ use pwt_macros::builder;
//mod services;
//pub use services::Services;
-use proxmox_yew_comp::{AptPackageManager, AptRepositories, ExistingProduct, Syslog, Tasks};
+use proxmox_yew_comp::{
+ AptPackageManager, AptRepositories, ExistingProduct, NodeStatusPanel, Syslog, Tasks,
+};
#[derive(Clone, PartialEq, Properties)]
#[builder]
@@ -67,6 +69,17 @@ impl Component for PdmServerAdministration {
|_| Services::new().into(),
)
*/
+ .with_item_builder(
+ TabBarItem::new()
+ .key("status")
+ .label(tr!("Status"))
+ .icon_class("fa fa-book"),
+ move |_| {
+ NodeStatusPanel::new()
+ .status_base_url("/nodes/localhost/status")
+ .into()
+ },
+ )
.with_item_builder(
TabBarItem::new()
.key("updates")
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 7+ messages in thread* [pdm-devel] [RFC PATCH datacenter-manager 3/3] nodes: remove unnecessary rustfmt::skip macro
2025-10-28 16:44 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp 0/6] add node status panel to proxmox datacenter manager Shannon Sterz
` (4 preceding siblings ...)
2025-10-28 16:44 ` [pdm-devel] [RFC PATCH datacenter-manager 2/3] ui: add NodeStatusPanel to the administration menu Shannon Sterz
@ 2025-10-28 16:44 ` Shannon Sterz
5 siblings, 0 replies; 7+ messages in thread
From: Shannon Sterz @ 2025-10-28 16:44 UTC (permalink / raw)
To: pdm-devel
with more items being present, rustfmt formats this list correctly.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
just a suggestion feel free to ignore
server/src/api/nodes/mod.rs | 1 -
1 file changed, 1 deletion(-)
diff --git a/server/src/api/nodes/mod.rs b/server/src/api/nodes/mod.rs
index f70fcaf..a0fe14a 100644
--- a/server/src/api/nodes/mod.rs
+++ b/server/src/api/nodes/mod.rs
@@ -36,7 +36,6 @@ pub const ITEM_ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);
-#[rustfmt::skip] // it'll put both entries on 1 line...
#[sortable]
pub const SUBDIRS: SubdirMap = &sorted!([
("apt", &apt::ROUTER),
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 7+ messages in thread