From: Erik Fastermann <e.fastermann@proxmox.com>
To: pbs-devel@lists.proxmox.com, pdm-devel@lists.proxmox.com
Cc: Erik Fastermann <e.fastermann@proxmox.com>
Subject: [PATCH proxmox 1/3] system-report: add crate for shared report generation
Date: Tue, 14 Jul 2026 10:31:54 +0200 [thread overview]
Message-ID: <20260714083156.99794-2-e.fastermann@proxmox.com> (raw)
In-Reply-To: <20260714083156.99794-1-e.fastermann@proxmox.com>
Factor the shared report generation from PBS and PDM into a new
proxmox-system-report crate, so both generate their reports from a
single implementation.
The crate defines the common section order (FILES, COMMANDS,
FUNCTIONS) and the set of general commands run on every product;
products pass their own additional files, commands and functions on
top. It produces no report on its own, so the user-visible changes
land in the respective server packages.
Suggested-by: Lukas Wagner <l.wagner@proxmox.com>
Suggested-by: Christian Ebner <c.ebner@proxmox.com>
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
Cargo.toml | 2 +
proxmox-system-report/Cargo.toml | 16 ++
proxmox-system-report/debian/copyright | 18 ++
proxmox-system-report/debian/debcargo.toml | 7 +
proxmox-system-report/src/lib.rs | 235 +++++++++++++++++++++
5 files changed, 278 insertions(+)
create mode 100644 proxmox-system-report/Cargo.toml
create mode 100644 proxmox-system-report/debian/copyright
create mode 100644 proxmox-system-report/debian/debcargo.toml
create mode 100644 proxmox-system-report/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index ef407166..1732444b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -59,6 +59,7 @@ members = [
"proxmox-subscription",
"proxmox-sys",
"proxmox-syslog-api",
+ "proxmox-system-report",
"proxmox-systemd",
"proxmox-tfa",
"proxmox-time",
@@ -177,6 +178,7 @@ proxmox-io = { version = "1.2.1", path = "proxmox-io" }
proxmox-lang = { version = "1.5", path = "proxmox-lang" }
proxmox-log = { version = "1.0.0", path = "proxmox-log" }
proxmox-login = { version = "1.0.0", path = "proxmox-login" }
+proxmox-network-api = { version = "1.0.5", path = "proxmox-network-api" }
proxmox-network-types = { version = "1.0.2", path = "proxmox-network-types" }
proxmox-parallel-handler = { version = "1.0.0", path = "proxmox-parallel-handler" }
proxmox-pgp = { version = "1.0.0", path = "proxmox-pgp" }
diff --git a/proxmox-system-report/Cargo.toml b/proxmox-system-report/Cargo.toml
new file mode 100644
index 00000000..84a87c76
--- /dev/null
+++ b/proxmox-system-report/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "proxmox-system-report"
+description = "Shared system report functionality"
+version = "0.1.0"
+
+authors.workspace = true
+edition.workspace = true
+exclude.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+proxmox-network-api = { workspace = true, features = ["impl"] }
+proxmox-sys.workspace = true
diff --git a/proxmox-system-report/debian/copyright b/proxmox-system-report/debian/copyright
new file mode 100644
index 00000000..77952eba
--- /dev/null
+++ b/proxmox-system-report/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2026 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-system-report/debian/debcargo.toml b/proxmox-system-report/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-system-report/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-system-report/src/lib.rs b/proxmox-system-report/src/lib.rs
new file mode 100644
index 00000000..92ca8e5d
--- /dev/null
+++ b/proxmox-system-report/src/lib.rs
@@ -0,0 +1,235 @@
+//! Shared functionality to generate system reports.
+
+#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
+#![deny(unsafe_code)]
+#![deny(missing_docs)]
+
+use std::fmt::Write;
+use std::path::Path;
+use std::process::Command;
+
+use proxmox_network_api::NetworkInterfaceType;
+
+/// A group of files to include: `(group_name, [path, ...])`.
+pub type FileGroup = (&'static str, Vec<&'static str>);
+
+/// A command to run: `(command, [arg, ...])`.
+pub type CommandSpec = (&'static str, Vec<&'static str>);
+
+/// A command to run with owned arguments: `(command, [arg, ...])`.
+type DynamicCommandSpec = (&'static str, Vec<String>);
+
+/// A function to run and its label: `(description, function)`.
+pub type FunctionMapping = (&'static str, fn() -> String);
+
+fn get_top_processes() -> String {
+ let (exe, args) = ("top", vec!["-b", "-c", "-w512", "-n", "1", "-o", "TIME"]);
+ let output = Command::new(exe).args(&args).output();
+ let output = match output {
+ Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
+ Err(err) => err.to_string(),
+ };
+ let output = output.lines().take(30).collect::<Vec<&str>>().join("\n");
+ format!("$ `{exe} {}`\n```\n{output}\n```", args.join(" "))
+}
+
+fn files() -> Vec<FileGroup> {
+ vec![(
+ "General System Info",
+ vec![
+ "/etc/hostname",
+ "/etc/hosts",
+ "/etc/network/interfaces",
+ "/etc/apt/sources.list",
+ "/etc/apt/sources.list.d/",
+ "/proc/pressure/",
+ ],
+ )]
+}
+
+fn commands() -> Vec<CommandSpec> {
+ vec![
+ ("proxmox-boot-tool", vec!["status"]),
+ ("df", vec!["-h", "-T"]),
+ (
+ "lsblk",
+ vec![
+ "--ascii",
+ "-M",
+ "-o",
+ "+HOTPLUG,ROTA,PHY-SEC,FSTYPE,MODEL,TRAN",
+ ],
+ ),
+ ("bash", vec!["-c", "ls -l /dev/disk/by-*/"]),
+ ("zpool", vec!["status"]),
+ ("zfs", vec!["list"]),
+ ("zarcstat", vec![]),
+ ("dmidecode", vec!["-t", "bios"]),
+ ("lscpu", vec![]),
+ ("lspci", vec!["-nnk"]),
+ ("ip", vec!["-details", "-statistics", "a"]),
+ ("ip", vec!["-4", "route", "show"]),
+ ("ip", vec!["-6", "route", "show"]),
+ ]
+}
+
+fn dynamic_commands() -> Vec<DynamicCommandSpec> {
+ let mut commands = Vec::new();
+
+ match proxmox_network_api::config() {
+ Ok((config, _)) => {
+ for (name, iface) in config.interfaces {
+ if iface.interface_type == NetworkInterfaceType::Eth {
+ commands.push(("ethtool", vec![name]));
+ }
+ }
+ }
+ Err(err) => {
+ eprintln!("failed to query network interfaces: {err}");
+ }
+ }
+
+ commands
+}
+
+fn function_calls() -> Vec<FunctionMapping> {
+ vec![("System Load & Uptime", get_top_processes)]
+}
+
+fn get_file_content(file: impl AsRef<Path>) -> String {
+ use proxmox_sys::fs::file_read_optional_string;
+ let content = match file_read_optional_string(&file) {
+ Ok(Some(content)) => content,
+ Ok(None) => String::from("# file does not exist"),
+ Err(err) => err.to_string(),
+ };
+ let file_name = file.as_ref().display();
+ format!("`$ cat '{file_name}'`\n```\n{}\n```", content.trim_end())
+}
+
+fn get_directory_content(path: impl AsRef<Path>) -> String {
+ let read_dir_iter = match std::fs::read_dir(&path) {
+ Ok(iter) => iter,
+ Err(err) => {
+ return format!(
+ "`$ cat '{}*'`\n```\n# read dir failed - {err}\n```",
+ path.as_ref().display(),
+ );
+ }
+ };
+ let mut out = String::new();
+ let mut first = true;
+ for entry in read_dir_iter {
+ let entry = match entry {
+ Ok(entry) => entry,
+ Err(err) => {
+ let _ = writeln!(out, "error during read-dir - {err}");
+ continue;
+ }
+ };
+ let path = entry.path();
+ if path.is_file() {
+ if first {
+ let _ = writeln!(out, "{}", get_file_content(path));
+ first = false;
+ } else {
+ let _ = writeln!(out, "\n{}", get_file_content(path));
+ }
+ } else {
+ let _ = writeln!(out, "skipping sub-directory `{}`", path.display());
+ }
+ }
+ out
+}
+
+fn get_command_output(exe: &str, args: &[&str]) -> String {
+ let output = Command::new(exe)
+ .env("PROXMOX_OUTPUT_NO_BORDER", "1")
+ .args(args)
+ .output();
+ let output = match output {
+ Ok(output) => {
+ let mut out = String::from_utf8_lossy(&output.stdout)
+ .trim_end()
+ .to_string();
+ let stderr = String::from_utf8_lossy(&output.stderr)
+ .trim_end()
+ .to_string();
+ if !stderr.is_empty() {
+ let _ = writeln!(out, "\n```\nSTDERR:\n```\n{stderr}");
+ }
+ out
+ }
+ Err(err) => err.to_string(),
+ };
+ format!("$ `{exe} {}`\n```\n{output}\n```", args.join(" "))
+}
+
+/// Generate a system report as a Markdown-like document.
+///
+/// The report has three top-level sections, always emitted in this order:
+/// `FILES`, `COMMANDS` and `FUNCTIONS`. The project-specific entries passed in
+/// are merged with a set of built-in, general-purpose entries common to all
+/// products.
+///
+/// Missing files, unreadable directories and commands that fail to spawn are
+/// reported inline instead of aborting the report.
+pub fn generate_report(
+ project_files: Vec<FileGroup>,
+ project_commands: Vec<CommandSpec>,
+ project_function_calls: Vec<FunctionMapping>,
+) -> String {
+ // We deliberately output the shared files before the project specific files
+ // to preserve the legacy report ordering.
+ let file_contents = files()
+ .iter()
+ .chain(&project_files)
+ .map(|group| {
+ let (group, files) = group;
+ let group_content = files
+ .iter()
+ .map(|file_name| {
+ let path = Path::new(file_name);
+ if path.is_dir() {
+ get_directory_content(path)
+ } else {
+ get_file_content(file_name)
+ }
+ })
+ .collect::<Vec<String>>()
+ .join("\n\n");
+
+ format!("### {group}\n\n{group_content}")
+ })
+ .collect::<Vec<String>>()
+ .join("\n\n");
+
+ let static_command_outputs = project_commands
+ .into_iter()
+ .chain(commands())
+ .map(|(command, args)| get_command_output(command, &args));
+
+ let dynamic_command_outputs = dynamic_commands().into_iter().map(|(command, args)| {
+ let args: Vec<_> = args.iter().map(String::as_str).collect();
+ get_command_output(command, &args)
+ });
+
+ let command_outputs = static_command_outputs
+ .chain(dynamic_command_outputs)
+ .collect::<Vec<String>>()
+ .join("\n\n");
+
+ let function_outputs = project_function_calls
+ .iter()
+ .chain(&function_calls())
+ .map(|(desc, function)| {
+ let output = function();
+ format!("#### {desc}\n{}\n", output.trim_end())
+ })
+ .collect::<Vec<String>>()
+ .join("\n\n");
+
+ format!(
+ "## FILES\n\n{file_contents}\n## COMMANDS\n\n{command_outputs}\n## FUNCTIONS\n\n{function_outputs}\n"
+ )
+}
--
2.47.3
next prev parent reply other threads:[~2026-07-14 8:32 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-14 8:31 [PATCH proxmox{,-backup,-datacenter-manager} 0/3] factor system report into shared crate Erik Fastermann
2026-07-14 8:31 ` Erik Fastermann [this message]
2026-07-14 8:31 ` [PATCH proxmox-backup 2/3] report: use shared proxmox-system-report crate Erik Fastermann
2026-07-14 8:31 ` [PATCH proxmox-datacenter-manager 3/3] " Erik Fastermann
2026-07-14 9:05 ` [PATCH proxmox{,-backup,-datacenter-manager} 0/3] factor system report into shared crate Erik Fastermann
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=20260714083156.99794-2-e.fastermann@proxmox.com \
--to=e.fastermann@proxmox.com \
--cc=pbs-devel@lists.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