From: Christian Ebner <c.ebner@proxmox.com>
To: Erik Fastermann <e.fastermann@proxmox.com>,
pbs-devel@lists.proxmox.com, pdm-devel@lists.proxmox.com
Cc: Lukas Wagner <l.wagner@proxmox.com>
Subject: Re: [PATCH proxmox 1/3] system-report: add crate for shared report generation
Date: Tue, 28 Jul 2026 15:59:39 +0200 [thread overview]
Message-ID: <1219b0fa-c7c8-4749-9069-9cb440338e91@proxmox.com> (raw)
In-Reply-To: <20260714083156.99794-2-e.fastermann@proxmox.com>
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"
> + )
> +}
WARNING: multiple messages have this Message-ID (diff)
From: Christian Ebner <c.ebner@proxmox.com>
To: Erik Fastermann <e.fastermann@proxmox.com>,
pbs-devel@lists.proxmox.com, pdm-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox 1/3] system-report: add crate for shared report generation
Date: Tue, 28 Jul 2026 15:59:39 +0200 [thread overview]
Message-ID: <1219b0fa-c7c8-4749-9069-9cb440338e91@proxmox.com> (raw)
In-Reply-To: <20260714083156.99794-2-e.fastermann@proxmox.com>
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"
> + )
> +}
next prev parent reply other threads:[~2026-07-28 13:59 UTC|newest]
Thread overview: 20+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-14 8:31 [PATCH proxmox{,-backup,-datacenter-manager} 0/3] factor system report into shared crate Erik Fastermann
2026-07-14 8:31 ` [PATCH proxmox 1/3] system-report: add crate for shared report generation Erik Fastermann
2026-07-14 8:31 ` Erik Fastermann
2026-07-28 13:59 ` Christian Ebner [this message]
2026-07-28 13:59 ` Christian Ebner
2026-07-29 11:55 ` Erik Fastermann
2026-07-29 11:55 ` Erik Fastermann
2026-07-29 12:08 ` Christian Ebner
2026-07-29 12:08 ` Christian Ebner
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-28 13:59 ` Christian Ebner
2026-07-14 8:31 ` [PATCH proxmox-datacenter-manager 3/3] " Erik Fastermann
2026-07-14 8:31 ` Erik Fastermann
2026-07-28 13:59 ` Christian Ebner
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
2026-07-28 13:59 ` Christian Ebner
2026-07-30 11:14 ` superseded: " Erik Fastermann
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=1219b0fa-c7c8-4749-9069-9cb440338e91@proxmox.com \
--to=c.ebner@proxmox.com \
--cc=e.fastermann@proxmox.com \
--cc=l.wagner@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is 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.