From: Shannon Sterz <s.sterz@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH proxmox 1/1] node-status: add node status crate
Date: Thu, 6 Nov 2025 13:43:29 +0100 [thread overview]
Message-ID: <20251106124334.189955-2-s.sterz@proxmox.com> (raw)
In-Reply-To: <20251106124334.189955-1-s.sterz@proxmox.com>
this includes api endpoints for querying api endpoints. the original
implementation was factored out from proxmox-backup.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
Tested-by: Dominik Csapak <d.csapak@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 1330fe97..37e8696c 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..4d65d216
--- /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 set certificate path 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
next prev parent reply other threads:[~2025-11-06 12:43 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-11-06 12:43 [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp 0/6] add node status panel to proxmox datacenter manager Shannon Sterz
2025-11-06 12:43 ` Shannon Sterz [this message]
2025-11-06 12:43 ` [pdm-devel] [PATCH yew-comp 1/2] node info: extend NodeStatus enum to include NodeStatus from proxmox-rs Shannon Sterz
2025-11-06 12:43 ` [pdm-devel] [PATCH yew-comp 2/2] node status panel: add a panel that show the current status of a node Shannon Sterz
2025-11-06 12:43 ` [pdm-devel] [PATCH datacenter-manager 1/3] api-types/api: add endpoints for querying the node's status Shannon Sterz
2025-11-06 12:43 ` [pdm-devel] [PATCH datacenter-manager 2/3] ui: make NodeStatusPanel available as a widget Shannon Sterz
2025-11-06 12:43 ` [pdm-devel] [PATCH datacenter-manager 3/3] nodes: remove unnecessary rustfmt::skip macro Shannon Sterz
2025-11-06 12:44 ` [pdm-devel] [PATCH datacenter-manager/proxmox/yew-comp 0/6] add node status panel to proxmox datacenter manager Shannon Sterz
-- strict thread matches above, loose matches on Subject: below --
2025-10-28 16:44 Shannon Sterz
2025-10-28 16:44 ` [pdm-devel] [PATCH proxmox 1/1] node-status: add node status crate Shannon Sterz
2025-11-03 12:50 ` Dominik Csapak
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=20251106124334.189955-2-s.sterz@proxmox.com \
--to=s.sterz@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox