[pdm-devel] [PATCH proxmox 1/1] node-status: add node status crate
Dominik Csapak
d.csapak at proxmox.com
Mon Nov 3 13:50:24 CET 2025
nit: would be nice to have in the commit message where this is from
(proxmox-backup in this case) and that we might want
to change from the implementation there to this.
Aside from that and the nit inline, looks good to me,
consider it:
Reviewed-by: Dominik Csapak <d.csapak at proxmox.com>
On 10/28/25 5:45 PM, Shannon Sterz wrote:
> this includes api endpoints for querying api endpoints
>
> Signed-off-by: Shannon Sterz <s.sterz at 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 at 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 at 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 at 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 at 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!"))
nit: the string is wrong, seems to be a leftover of a copy&paste
> +}
> +
> +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,
> +}
More information about the pdm-devel
mailing list