From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id A73491FF17A for ; Tue, 28 Oct 2025 17:44:44 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 4876720384; Tue, 28 Oct 2025 17:45:17 +0100 (CET) From: Shannon Sterz To: pdm-devel@lists.proxmox.com Date: Tue, 28 Oct 2025 17:44:30 +0100 Message-ID: <20251028164435.576642-2-s.sterz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251028164435.576642-1-s.sterz@proxmox.com> References: <20251028164435.576642-1-s.sterz@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1761669866183 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.058 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_SHORT 0.001 Use of a URL Shortener for very short URL SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pdm-devel] [PATCH proxmox 1/1] node-status: add node status crate X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" this includes api endpoints for querying api endpoints Signed-off-by: Shannon Sterz --- 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 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 , + rustc:native (>= 1.82) , + libstd-rust-dev , + 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 +Maintainer: Proxmox Support Team +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 +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 . 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 " + +[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 = OnceLock::new(); + +pub fn init_node_status_api>(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 { + 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::>() + .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 { + 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