* [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-28 13:59 ` Christian Ebner
2026-07-14 8:31 ` [PATCH proxmox-backup 2/3] report: use shared proxmox-system-report crate Erik Fastermann
` (4 subsequent siblings)
5 siblings, 1 reply; 12+ 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] 12+ messages in thread* Re: [PATCH proxmox 1/3] system-report: add crate for shared report generation
2026-07-14 8:31 ` [PATCH proxmox 1/3] system-report: add crate for shared report generation Erik Fastermann
@ 2026-07-28 13:59 ` Christian Ebner
2026-07-29 11:55 ` Erik Fastermann
0 siblings, 1 reply; 12+ messages in thread
From: Christian Ebner @ 2026-07-28 13:59 UTC (permalink / raw)
To: Erik Fastermann, pbs-devel, pdm-devel
On 7/14/26 10:31 AM, Erik Fastermann wrote:
> 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"
nit: let's start with 1.0.0, otherwise this could be mis-interpreted as
not production-ready.
> +
> +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
comment: these dependencies must also be declared in debian/control
(still missing and should be included to build deb packages with their
dependencies), so apt can resolve and install these in case no other
package does.
> 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>
question: not sure about the copyright timespan here, but since the
factored out code lived in proxmox-backup and that has 2019-2026 this is
fine I guess?
> +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>);
nit: IMHO this should be StaticArgsCommandSpec, and the docstring state
that this is for commands with static, compile time known arguments ...
> +
> +/// A command to run with owned arguments: `(command, [arg, ...])`.
> +type DynamicCommandSpec = (&'static str, Vec<String>);
... and this DynamicArgsCommmandSpec, the docstring mentioning that
these commands are to be run with dynamically generated arguments
produced on runtime.
> +
> +/// 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> {
nit: in PBS and PDM, the files() helper generates also the list of
additional, product specific file groups. So might be better to call
this product_common_files() or product_agnostic_files()?
> + 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> {
nit: same as for files(), the command() helper should reflect that these
are product common commands, so maybe rename this to
product_common_commands()?
> + 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"]),
comment: these commands depend on external executables. So there should
be a dependency in debian/control as well.
> + ]
> +}
> +
> +fn dynamic_commands() -> Vec<DynamicCommandSpec> {
nit: same as files() and commands() above ...
> + 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> {
nit: same as for others above, this are the product_common_function_calls()
> + 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"
> + )
> +}
^ permalink raw reply [flat|nested] 12+ messages in thread* Re: [PATCH proxmox 1/3] system-report: add crate for shared report generation
2026-07-28 13:59 ` Christian Ebner
@ 2026-07-29 11:55 ` Erik Fastermann
2026-07-29 12:08 ` Christian Ebner
0 siblings, 1 reply; 12+ messages in thread
From: Erik Fastermann @ 2026-07-29 11:55 UTC (permalink / raw)
To: Christian Ebner, pbs-devel, pdm-devel
Thank you for the review. Some notes/questions below. I will send the v2
ASAP.
[snip]
>> 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>
>
> question: not sure about the copyright timespan here, but since the
> factored out code lived in proxmox-backup and that has 2019-2026 this is
> fine I guess?
I checked and for the recently added `proxmox-disks` only 'Copyright:
2026' is used and that crate was also factored out from PBS, so I will
do the same.
[snip]
>> + 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"]),
>
> comment: these commands depend on external executables. So there should
> be a dependency in debian/control as well.
E.g. `proxmox-disks` calls `zpool` and `sgdisk`, which are also not in
the `Depends` section. In fact all crates I checked in this repository
only have other Rust related `Depends`. It looks like either PBS or PDM
depend on them directly or they are pulled in by the installer.
The `Depends` section of PDM or PBS might need some work in general.
E.g. `iproute2` is missing from PDM `Depends`, but is given for PBS. I
saw there was a patch which added some to PBS [0], but that was not
applied to PDM. It would probably be good to check what dependencies
should be added. Should this be part of this series? What do you think?
[snip]
[0]:
https://lore.proxmox.com/all/d7bd1b5d-dc09-4eac-9de5-4382160da88f@proxmox.com
^ permalink raw reply [flat|nested] 12+ messages in thread* Re: [PATCH proxmox 1/3] system-report: add crate for shared report generation
2026-07-29 11:55 ` Erik Fastermann
@ 2026-07-29 12:08 ` Christian Ebner
0 siblings, 0 replies; 12+ messages in thread
From: Christian Ebner @ 2026-07-29 12:08 UTC (permalink / raw)
To: Erik Fastermann, pbs-devel, pdm-devel
On 7/29/26 1:55 PM, Erik Fastermann wrote:
> Thank you for the review. Some notes/questions below. I will send the v2
> ASAP.
>
> [snip]
>
>>> 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>
>>
>> question: not sure about the copyright timespan here, but since the
>> factored out code lived in proxmox-backup and that has 2019-2026 this
>> is fine I guess?
>
> I checked and for the recently added `proxmox-disks` only 'Copyright:
> 2026' is used and that crate was also factored out from PBS, so I will
> do the same.
>
> [snip]
>
>>> + 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"]),
>>
>> comment: these commands depend on external executables. So there
>> should be a dependency in debian/control as well.
>
> E.g. `proxmox-disks` calls `zpool` and `sgdisk`, which are also not in
> the `Depends` section. In fact all crates I checked in this repository
> only have other Rust related `Depends`. It looks like either PBS or PDM
> depend on them directly or they are pulled in by the installer.
Right, since this is a lib only and does not produce any executable by
itself, it is not correct to pull these in. Disregard my comment with
respect to that.
> The `Depends` section of PDM or PBS might need some work in general.
> E.g. `iproute2` is missing from PDM `Depends`, but is given for PBS. I
> saw there was a patch which added some to PBS [0], but that was not
> applied to PDM. It would probably be good to check what dependencies
> should be added. Should this be part of this series? What do you think?
Yes, adding a patch to assure such dependencies is especially important
if PBS or PDM are installed on top of a vanilla Debian and not via the
installer.
^ permalink raw reply [flat|nested] 12+ 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 ` [PATCH proxmox 1/3] system-report: add crate for shared report generation Erik Fastermann
@ 2026-07-14 8:31 ` Erik Fastermann
2026-07-28 13:59 ` Christian Ebner
2026-07-14 8:31 ` [PATCH proxmox-datacenter-manager 3/3] " Erik Fastermann
` (3 subsequent siblings)
5 siblings, 1 reply; 12+ 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] 12+ messages in thread* Re: [PATCH proxmox-backup 2/3] report: use shared proxmox-system-report crate
2026-07-14 8:31 ` [PATCH proxmox-backup 2/3] report: use shared proxmox-system-report crate Erik Fastermann
@ 2026-07-28 13:59 ` Christian Ebner
0 siblings, 0 replies; 12+ messages in thread
From: Christian Ebner @ 2026-07-28 13:59 UTC (permalink / raw)
To: Erik Fastermann, pbs-devel, pdm-devel
On 7/14/26 10:31 AM, Erik Fastermann wrote:
> 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"
nit: as motivated in patch 1, lets start with 1.0 here.... also, this
requires a build dependency in debian/control on the corresponding dev
package, so it is pulled in by apt.
> 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> {
nit: maybe better call this pbs_specific_files() or
product_specific_files(), just to make it clearer that this is only part
of what's to be generated.
> 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> {
nit: same as above for files()
> 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> {
nit: same as above for files() and commands()...
> - 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())
> }
^ permalink raw reply [flat|nested] 12+ 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 ` [PATCH proxmox 1/3] system-report: add crate for shared report generation 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-28 13:59 ` Christian Ebner
2026-07-14 9:05 ` [PATCH proxmox{,-backup,-datacenter-manager} 0/3] factor system report into shared crate Erik Fastermann
` (2 subsequent siblings)
5 siblings, 1 reply; 12+ 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] 12+ messages in thread* Re: [PATCH proxmox-datacenter-manager 3/3] report: use shared proxmox-system-report crate
2026-07-14 8:31 ` [PATCH proxmox-datacenter-manager 3/3] " Erik Fastermann
@ 2026-07-28 13:59 ` Christian Ebner
0 siblings, 0 replies; 12+ messages in thread
From: Christian Ebner @ 2026-07-28 13:59 UTC (permalink / raw)
To: Erik Fastermann, pbs-devel, pdm-devel
On 7/14/26 10:31 AM, Erik Fastermann wrote:
> 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>
same comments as for patch 2 apply here as well.
^ permalink raw reply [flat|nested] 12+ 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 ` [PATCH proxmox-datacenter-manager 3/3] " Erik Fastermann
@ 2026-07-14 9:05 ` Erik Fastermann
2026-07-28 13:59 ` Christian Ebner
2026-07-30 11:14 ` superseded: " Erik Fastermann
5 siblings, 0 replies; 12+ 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] 12+ 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
` (3 preceding siblings ...)
2026-07-14 9:05 ` [PATCH proxmox{,-backup,-datacenter-manager} 0/3] factor system report into shared crate Erik Fastermann
@ 2026-07-28 13:59 ` Christian Ebner
2026-07-30 11:14 ` superseded: " Erik Fastermann
5 siblings, 0 replies; 12+ messages in thread
From: Christian Ebner @ 2026-07-28 13:59 UTC (permalink / raw)
To: Erik Fastermann, pbs-devel, pdm-devel
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.
Thanks for this patches, cleanups look in general good to me! Only left
a few comments on individual patches, especially with respect to
packaging, which warrant to be revisited IMO.
^ permalink raw reply [flat|nested] 12+ messages in thread* superseded: [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
` (4 preceding siblings ...)
2026-07-28 13:59 ` Christian Ebner
@ 2026-07-30 11:14 ` Erik Fastermann
5 siblings, 0 replies; 12+ messages in thread
From: Erik Fastermann @ 2026-07-30 11:14 UTC (permalink / raw)
To: pbs-devel, pdm-devel
Superseded-by:
https://lore.proxmox.com/all/20260730101349.217371-1-e.fastermann@proxmox.com
^ permalink raw reply [flat|nested] 12+ messages in thread