all lists on lists.proxmox.com
 help / color / mirror / Atom feed
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: Tue, 28 Oct 2025 17:44:30 +0100	[thread overview]
Message-ID: <20251028164435.576642-2-s.sterz@proxmox.com> (raw)
In-Reply-To: <20251028164435.576642-1-s.sterz@proxmox.com>

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


  reply	other threads:[~2025-10-28 16:44 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
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 ` [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 ` [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 ` [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

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=20251028164435.576642-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 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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal