* [PATCH proxmox{,-backup,-datacenter-manager} 0/3] factor system report into shared crate
@ 2026-07-14 8:31 Erik Fastermann
2026-07-14 8:31 ` Erik Fastermann
` (3 more replies)
0 siblings, 4 replies; 8+ messages in thread
From: Erik Fastermann @ 2026-07-14 8:31 UTC (permalink / raw)
To: pbs-devel, pdm-devel; +Cc: Erik Fastermann
This series moves the system report logic that was duplicated in
PBS and PDM into a new shared proxmox-system-report crate.
proxmox:
Adds the proxmox-system-report crate holding the common file,
command and function collection, the unified section order
(FILES, COMMANDS, FUNCTIONS) and the merged set of general commands
run on every product.
proxmox-backup, proxmox-datacenter-manager:
Drop the duplicated logic and generate the report through the shared
crate, passing only their product specific files, commands and
functions.
The report content stays the same apart from two intentional effects
of sharing one implementation: the top-level section order is now the
same for both products, and the general command set is the union of
what each ran before. PDM additionally emits FILES first now instead
of last. Product specific entries are untouched.
proxmox:
Erik Fastermann (1):
system-report: add crate for shared report generation
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
proxmox-backup:
Erik Fastermann (1):
report: use shared proxmox-system-report crate
Cargo.toml | 2 +
src/server/report.rs | 213 +++----------------------------------------
2 files changed, 15 insertions(+), 200 deletions(-)
proxmox-datacenter-manager:
Erik Fastermann (1):
report: use shared proxmox-system-report crate
Cargo.toml | 1 +
server/Cargo.toml | 1 +
server/src/report.rs | 198 +------------------------------------------
3 files changed, 6 insertions(+), 194 deletions(-)
Summary over all repositories:
10 files changed, 299 insertions(+), 394 deletions(-)
--
Generated by murpp 0.11.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH proxmox 1/3] system-report: add crate for shared report generation
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
2026-07-14 8:31 ` Erik Fastermann
` (2 subsequent siblings)
3 siblings, 0 replies; 8+ messages in thread
From: Erik Fastermann @ 2026-07-14 8:31 UTC (permalink / raw)
To: pbs-devel, pdm-devel; +Cc: Erik Fastermann, Lukas Wagner
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
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH proxmox 1/3] system-report: add crate for shared report generation
@ 2026-07-14 8:31 ` Erik Fastermann
0 siblings, 0 replies; 8+ messages in thread
From: Erik Fastermann @ 2026-07-14 8:31 UTC (permalink / raw)
To: pbs-devel, pdm-devel; +Cc: Erik Fastermann
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
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH proxmox-backup 2/3] report: use shared proxmox-system-report crate
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
2026-07-14 8:31 ` Erik Fastermann
` (2 subsequent siblings)
3 siblings, 0 replies; 8+ messages in thread
From: Erik Fastermann @ 2026-07-14 8:31 UTC (permalink / raw)
To: pbs-devel, pdm-devel; +Cc: Erik Fastermann, Lukas Wagner
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
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH proxmox-backup 2/3] report: use shared proxmox-system-report crate
@ 2026-07-14 8:31 ` Erik Fastermann
0 siblings, 0 replies; 8+ messages in thread
From: Erik Fastermann @ 2026-07-14 8:31 UTC (permalink / raw)
To: pbs-devel, pdm-devel; +Cc: Erik Fastermann
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
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH proxmox-datacenter-manager 3/3] report: use shared proxmox-system-report crate
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
2026-07-14 8:31 ` Erik Fastermann
` (2 subsequent siblings)
3 siblings, 0 replies; 8+ messages in thread
From: Erik Fastermann @ 2026-07-14 8:31 UTC (permalink / raw)
To: pbs-devel, pdm-devel; +Cc: Erik Fastermann, Lukas Wagner
Move the common report logic to the proxmox-system-report crate and
keep only the PDM specific files and commands here.
User-visible output changes:
- section order is now FILES, COMMANDS, FUNCTIONS, matching PBS
(was COMMANDS, FUNCTIONS, FILES)
- adds dmidecode, lscpu and lspci output, previously PBS only
- the disk listing now covers all /dev/disk/by-* directories instead
of only by-id and by-path, matching PBS
Product specific entries and their contents are otherwise 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 | 1 +
server/Cargo.toml | 1 +
server/src/report.rs | 198 +------------------------------------------
3 files changed, 6 insertions(+), 194 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index ee96ae14..d68c78e1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -63,6 +63,7 @@ proxmox-simple-config = "1"
proxmox-sortable-macro = "1"
proxmox-subscription = { version = "1.0.2", features = [ "api-types"], default-features = false }
proxmox-sys = "1"
+proxmox-system-report = "0.1"
proxmox-systemd = "1"
proxmox-tfa = { version = "6", features = [ "api-types" ], default-features = false }
proxmox-time = "2"
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 663f21cb..e8b21d3c 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -61,6 +61,7 @@ proxmox-serde = { workspace = true, features = [ "serde_json" ] }
proxmox-sortable-macro.workspace = true
proxmox-subscription = { workspace = true, features = [ "api-types", "impl" ] }
proxmox-sys = { workspace = true, features = [ "timer" ] }
+proxmox-system-report.workspace = true
proxmox-systemd.workspace = true
proxmox-tfa = { workspace = true, features = [ "api" ] }
proxmox-time.workspace = true
diff --git a/server/src/report.rs b/server/src/report.rs
index 3c87b991..585df35a 100644
--- a/server/src/report.rs
+++ b/server/src/report.rs
@@ -1,36 +1,7 @@
-use std::fmt::Write;
-use std::path::Path;
-use std::process::Command;
+use proxmox_system_report::{CommandSpec, FileGroup};
-use proxmox_network_api::NetworkInterfaceType;
-
-// TODO: This was copied from PBS. Might make sense to refactor these a little
-// bit and move them a `proxmox-system-report` crate or something.
-
-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/",
- ],
- ),
(
"User & Access",
vec![
@@ -48,9 +19,8 @@ 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-datacenter-manager-admin",
@@ -65,169 +35,9 @@ fn commands() -> Vec<(&'static str, Vec<&'static str>)> {
"proxmox-datacenter-manager-admin",
vec!["support-status", "get"],
),
- ("proxmox-boot-tool", vec!["status"]),
- ("df", vec!["-h", "-T"]),
- (
- "lsblk",
- vec![
- "--ascii",
- "-M",
- "-o",
- "+HOTPLUG,ROTA,PHY-SEC,FSTYPE,MODEL,TRAN",
- ],
- ),
- ("ls", vec!["-l", "/dev/disk/by-id", "/dev/disk/by-path"]),
- ("zpool", vec!["status"]),
- ("zfs", vec!["list"]),
- ("zarcstat", vec![]),
- ("ip", vec!["-details", "-statistics", "address"]),
- ("ip", vec!["-4", "route", "show"]),
- ("ip", vec!["-6", "route", "show"]),
]
}
-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![("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: &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
- }
- Err(err) => err.to_string(),
- };
- format!("$ `{exe} {}`\n```\n{output}\n```", args.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!(
- "## COMMANDS \n\n {command_outputs}\n\n## FUNCTIONS\n\n{function_outputs}\n## FILES\n\n{file_contents}\n"
- )
+ proxmox_system_report::generate_report(files(), commands(), vec![])
}
--
2.47.3
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH proxmox-datacenter-manager 3/3] report: use shared proxmox-system-report crate
@ 2026-07-14 8:31 ` Erik Fastermann
0 siblings, 0 replies; 8+ messages in thread
From: Erik Fastermann @ 2026-07-14 8:31 UTC (permalink / raw)
To: pbs-devel, pdm-devel; +Cc: Erik Fastermann
Move the common report logic to the proxmox-system-report crate and
keep only the PDM specific files and commands here.
User-visible output changes:
- section order is now FILES, COMMANDS, FUNCTIONS, matching PBS
(was COMMANDS, FUNCTIONS, FILES)
- adds dmidecode, lscpu and lspci output, previously PBS only
- the disk listing now covers all /dev/disk/by-* directories instead
of only by-id and by-path, matching PBS
Product specific entries and their contents are otherwise 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 | 1 +
server/Cargo.toml | 1 +
server/src/report.rs | 198 +------------------------------------------
3 files changed, 6 insertions(+), 194 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index ee96ae14..d68c78e1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -63,6 +63,7 @@ proxmox-simple-config = "1"
proxmox-sortable-macro = "1"
proxmox-subscription = { version = "1.0.2", features = [ "api-types"], default-features = false }
proxmox-sys = "1"
+proxmox-system-report = "0.1"
proxmox-systemd = "1"
proxmox-tfa = { version = "6", features = [ "api-types" ], default-features = false }
proxmox-time = "2"
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 663f21cb..e8b21d3c 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -61,6 +61,7 @@ proxmox-serde = { workspace = true, features = [ "serde_json" ] }
proxmox-sortable-macro.workspace = true
proxmox-subscription = { workspace = true, features = [ "api-types", "impl" ] }
proxmox-sys = { workspace = true, features = [ "timer" ] }
+proxmox-system-report.workspace = true
proxmox-systemd.workspace = true
proxmox-tfa = { workspace = true, features = [ "api" ] }
proxmox-time.workspace = true
diff --git a/server/src/report.rs b/server/src/report.rs
index 3c87b991..585df35a 100644
--- a/server/src/report.rs
+++ b/server/src/report.rs
@@ -1,36 +1,7 @@
-use std::fmt::Write;
-use std::path::Path;
-use std::process::Command;
+use proxmox_system_report::{CommandSpec, FileGroup};
-use proxmox_network_api::NetworkInterfaceType;
-
-// TODO: This was copied from PBS. Might make sense to refactor these a little
-// bit and move them a `proxmox-system-report` crate or something.
-
-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/",
- ],
- ),
(
"User & Access",
vec![
@@ -48,9 +19,8 @@ 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-datacenter-manager-admin",
@@ -65,169 +35,9 @@ fn commands() -> Vec<(&'static str, Vec<&'static str>)> {
"proxmox-datacenter-manager-admin",
vec!["support-status", "get"],
),
- ("proxmox-boot-tool", vec!["status"]),
- ("df", vec!["-h", "-T"]),
- (
- "lsblk",
- vec![
- "--ascii",
- "-M",
- "-o",
- "+HOTPLUG,ROTA,PHY-SEC,FSTYPE,MODEL,TRAN",
- ],
- ),
- ("ls", vec!["-l", "/dev/disk/by-id", "/dev/disk/by-path"]),
- ("zpool", vec!["status"]),
- ("zfs", vec!["list"]),
- ("zarcstat", vec![]),
- ("ip", vec!["-details", "-statistics", "address"]),
- ("ip", vec!["-4", "route", "show"]),
- ("ip", vec!["-6", "route", "show"]),
]
}
-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![("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: &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
- }
- Err(err) => err.to_string(),
- };
- format!("$ `{exe} {}`\n```\n{output}\n```", args.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!(
- "## COMMANDS \n\n {command_outputs}\n\n## FUNCTIONS\n\n{function_outputs}\n## FILES\n\n{file_contents}\n"
- )
+ proxmox_system_report::generate_report(files(), commands(), vec![])
}
--
2.47.3
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH proxmox{,-backup,-datacenter-manager} 0/3] factor system report into shared crate
2026-07-14 8:31 [PATCH proxmox{,-backup,-datacenter-manager} 0/3] factor system report into shared crate Erik Fastermann
` (2 preceding siblings ...)
2026-07-14 8:31 ` Erik Fastermann
@ 2026-07-14 9:05 ` Erik Fastermann
3 siblings, 0 replies; 8+ messages in thread
From: Erik Fastermann @ 2026-07-14 9:05 UTC (permalink / raw)
To: pbs-devel, pdm-devel
Just noticed that this relies on unapplied patches for PBS [0] and PDM
[1]: fix #7187: report: add ethtool output for physical interfaces
[0]
https://lore.proxmox.com/pbs-devel/20260515083918.10590-1-e.fastermann@proxmox.com/
[1]
https://lore.proxmox.com/pdm-devel/20260515084116.8028-1-e.fastermann@proxmox.com/
On 7/14/26 10:32 AM, Erik Fastermann wrote:
> This series moves the system report logic that was duplicated in
> PBS and PDM into a new shared proxmox-system-report crate.
>
> proxmox:
> Adds the proxmox-system-report crate holding the common file,
> command and function collection, the unified section order
> (FILES, COMMANDS, FUNCTIONS) and the merged set of general commands
> run on every product.
>
> proxmox-backup, proxmox-datacenter-manager:
> Drop the duplicated logic and generate the report through the shared
> crate, passing only their product specific files, commands and
> functions.
>
> The report content stays the same apart from two intentional effects
> of sharing one implementation: the top-level section order is now the
> same for both products, and the general command set is the union of
> what each ran before. PDM additionally emits FILES first now instead
> of last. Product specific entries are untouched.
>
>
> proxmox:
>
> Erik Fastermann (1):
> system-report: add crate for shared report generation
>
> 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
>
>
> proxmox-backup:
>
> Erik Fastermann (1):
> report: use shared proxmox-system-report crate
>
> Cargo.toml | 2 +
> src/server/report.rs | 213 +++----------------------------------------
> 2 files changed, 15 insertions(+), 200 deletions(-)
>
>
> proxmox-datacenter-manager:
>
> Erik Fastermann (1):
> report: use shared proxmox-system-report crate
>
> Cargo.toml | 1 +
> server/Cargo.toml | 1 +
> server/src/report.rs | 198 +------------------------------------------
> 3 files changed, 6 insertions(+), 194 deletions(-)
>
>
> Summary over all repositories:
> 10 files changed, 299 insertions(+), 394 deletions(-)
>
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-07-14 9:05 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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
2026-07-14 8:31 ` [PATCH proxmox-backup 2/3] report: use shared proxmox-system-report crate Erik Fastermann
2026-07-14 8:31 ` Erik Fastermann
2026-07-14 8:31 ` [PATCH proxmox-datacenter-manager 3/3] " Erik Fastermann
2026-07-14 8:31 ` Erik Fastermann
2026-07-14 9:05 ` [PATCH proxmox{,-backup,-datacenter-manager} 0/3] factor system report into shared crate Erik Fastermann
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.