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-backup 2/3] report: use shared proxmox-system-report crate
Date: Tue, 14 Jul 2026 10:31:55 +0200 [thread overview]
Message-ID: <20260714083156.99794-3-e.fastermann@proxmox.com> (raw)
In-Reply-To: <20260714083156.99794-1-e.fastermann@proxmox.com>
Move the common report logic to the proxmox-system-report crate and
keep only the PBS specific files, commands and the Datastores
function here.
User-visible output changes:
- df now also shows the filesystem type (df -h -T), matching PDM
- adds the IPv4 and IPv6 route tables (ip -4/-6 route show),
previously PDM only
The section order (FILES, COMMANDS, FUNCTIONS) and all product
specific entries are unchanged.
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 +
src/server/report.rs | 213 +++----------------------------------------
2 files changed, 15 insertions(+), 200 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index a625370cf..fb7904d38 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -94,6 +94,7 @@ proxmox-sortable-macro = "1"
proxmox-subscription = { version = "1.0.2", features = [ "api-types" ] }
proxmox-syslog-api = { version = "1.1", features = [ "impl" ] }
proxmox-sys = "1"
+proxmox-system-report = "0.1"
proxmox-systemd = "1.0.1"
proxmox-tfa = { version = "6.0.3", features = [ "api", "api-types" ] }
proxmox-time = "2"
@@ -252,6 +253,7 @@ proxmox-sortable-macro.workspace = true
proxmox-subscription.workspace = true
proxmox-syslog-api.workspace = true
proxmox-sys = { workspace = true, features = [ "timer" ] }
+proxmox-system-report.workspace = true
proxmox-systemd.workspace = true
proxmox-tfa.workspace = true
proxmox-time.workspace = true
diff --git a/src/server/report.rs b/src/server/report.rs
index 297718d2e..442a8573d 100644
--- a/src/server/report.rs
+++ b/src/server/report.rs
@@ -1,33 +1,7 @@
-use std::fmt::Write;
-use std::path::Path;
-use std::process::Command;
+use proxmox_system_report::{CommandSpec, FileGroup, FunctionMapping};
-use proxmox_network_api::NetworkInterfaceType;
-
-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<(&'static str, Vec<&'static str>)> {
+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/",
- ],
- ),
(
"Datastores & Remotes",
vec!["/etc/proxmox-backup/datastore.cfg"],
@@ -65,192 +39,31 @@ fn files() -> Vec<(&'static str, Vec<&'static str>)> {
]
}
-fn commands() -> Vec<(&'static str, Vec<&'static str>)> {
+fn commands() -> Vec<CommandSpec> {
vec![
- // ("<command>", vec![<arg [, arg]>])
("date", vec!["-R"]),
("proxmox-backup-manager", vec!["versions", "--verbose"]),
("proxmox-backup-manager", vec!["subscription", "get"]),
("proxmox-backup-manager", vec!["ldap", "list"]),
("proxmox-backup-manager", vec!["openid", "list"]),
- ("proxmox-boot-tool", vec!["status"]),
- ("df", vec!["-h"]),
- (
- "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"]),
]
}
-fn dynamic_commands() -> Vec<(&'static str, Vec<String>)> {
- 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
-}
-
-// (description, function())
-type FunctionMapping = (&'static str, fn() -> String);
-
fn function_calls() -> Vec<FunctionMapping> {
- vec![
- ("Datastores", || {
- let config = match pbs_config::datastore::config() {
- Ok((config, _digest)) => config,
- _ => return String::from("could not read datastore config"),
- };
-
- let mut list = Vec::new();
- for store in config.sections.keys() {
- list.push(store.as_str());
- }
- format!("```\n{}\n```", list.join(", "))
- }),
- ("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;
- }
+ vec![("Datastores", || {
+ let config = match pbs_config::datastore::config() {
+ Ok((config, _digest)) => config,
+ _ => return String::from("could not read datastore config"),
};
- 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: &Vec<&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
+ let mut list = Vec::new();
+ for store in config.sections.keys() {
+ list.push(store.as_str());
}
- Err(err) => err.to_string(),
- };
- format!("$ `{exe} {}`\n```\n{output}\n```", args.join(" "))
+ format!("```\n{}\n```", list.join(", "))
+ })]
}
pub fn generate_report() -> String {
- let file_contents = files()
- .iter()
- .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 = commands()
- .into_iter()
- .map(|(command, args)| get_command_output(command, &args));
-
- let dynamic_command_outputs = dynamic_commands().into_iter().map(|(command, args)| {
- let args = 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 = function_calls()
- .iter()
- .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"
- )
+ proxmox_system_report::generate_report(files(), commands(), function_calls())
}
--
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 ` [PATCH proxmox 1/3] system-report: add crate for shared report generation Erik Fastermann
2026-07-14 8:31 ` Erik Fastermann [this message]
2026-07-14 8:31 ` [PATCH proxmox-datacenter-manager 3/3] report: use shared proxmox-system-report crate 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-3-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