From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 391CF1FF0E1 for ; Mon, 10 Aug 2026 14:29:08 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 8EA4B2168E; Mon, 10 Aug 2026 14:29:07 +0200 (CEST) Message-ID: <4d0f692b-29a9-4951-830e-efe145f7bbdd@proxmox.com> Date: Mon, 10 Aug 2026 14:28:59 +0200 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: [PATCH proxmox v2 1/5] system-report: add crate for shared report generation To: Erik Fastermann , pbs-devel@lists.proxmox.com, pdm-devel@lists.proxmox.com References: <20260730101349.217371-1-e.fastermann@proxmox.com> <20260730101349.217371-2-e.fastermann@proxmox.com> Content-Language: en-US, de-DE From: Christian Ebner In-Reply-To: <20260730101349.217371-2-e.fastermann@proxmox.com> Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1786364928096 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.892 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) KAM_SHORT 0.001 Use of a URL Shortener for very short URL RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: DNHMHHTLNSAT2TBUQ74XHUMVBBZPDKQO X-Message-ID-Hash: DNHMHHTLNSAT2TBUQ74XHUMVBBZPDKQO X-MailFrom: c.ebner@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: On 7/30/26 12:13 PM, 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 > Suggested-by: Christian Ebner > Signed-off-by: Erik Fastermann > --- > Cargo.toml | 2 + > proxmox-system-report/Cargo.toml | 16 ++ > proxmox-system-report/debian/changelog | 6 + > proxmox-system-report/debian/control | 36 ++++ > proxmox-system-report/debian/copyright | 18 ++ > proxmox-system-report/debian/debcargo.toml | 7 + > proxmox-system-report/src/lib.rs | 239 +++++++++++++++++++++ > 7 files changed, 324 insertions(+) > create mode 100644 proxmox-system-report/Cargo.toml > create mode 100644 proxmox-system-report/debian/changelog > create mode 100644 proxmox-system-report/debian/control > 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..3efee984 > --- /dev/null > +++ b/proxmox-system-report/Cargo.toml > @@ -0,0 +1,16 @@ > +[package] > +name = "proxmox-system-report" > +description = "Shared system report functionality" > +version = "1.0.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/changelog b/proxmox-system-report/debian/changelog > new file mode 100644 > index 00000000..cfdfc030 > --- /dev/null > +++ b/proxmox-system-report/debian/changelog > @@ -0,0 +1,6 @@ > +rust-proxmox-system-report (1.0.0-1) trixie; urgency=medium > + > + * initial release, factored out from proxmox-backup and > + proxmox-datacenter-manager. > + > + -- Proxmox Support Team Wed, 29 Jul 2026 10:40:07 +0200 nit: best left out and to be added by the maintainer, as already outdated. > diff --git a/proxmox-system-report/debian/control b/proxmox-system-report/debian/control > new file mode 100644 > index 00000000..b764f690 > --- /dev/null > +++ b/proxmox-system-report/debian/control > @@ -0,0 +1,36 @@ > +Source: rust-proxmox-system-report > +Section: rust > +Priority: optional > +Build-Depends: debhelper-compat (= 13), > + dh-sequence-cargo > +Build-Depends-Arch: cargo:native , > + rustc:native (>= 1.85) , > + libstd-rust-dev , > + librust-proxmox-network-api-1+default-dev (>= 1.0.5-~~) , > + librust-proxmox-network-api-1+impl-dev (>= 1.0.5-~~) , > + librust-proxmox-sys-1+default-dev (>= 1.0.1-~~) > +Maintainer: Proxmox Support Team > +Standards-Version: 4.7.2 > +Vcs-Git: git://git.proxmox.com/git/proxmox.git > +Vcs-Browser: https://git.proxmox.com/?p=proxmox.git > +Homepage: https://proxmox.com > +X-Cargo-Crate: proxmox-system-report > + > +Package: librust-proxmox-system-report-dev > +Architecture: any > +Multi-Arch: same > +Depends: > + ${misc:Depends}, > + librust-proxmox-network-api-1+default-dev (>= 1.0.5-~~), > + librust-proxmox-network-api-1+impl-dev (>= 1.0.5-~~), > + librust-proxmox-sys-1+default-dev (>= 1.0.1-~~) > +Provides: > + librust-proxmox-system-report+default-dev (= ${binary:Version}), > + librust-proxmox-system-report-1-dev (= ${binary:Version}), > + librust-proxmox-system-report-1+default-dev (= ${binary:Version}), > + librust-proxmox-system-report-1.0-dev (= ${binary:Version}), > + librust-proxmox-system-report-1.0+default-dev (= ${binary:Version}), > + librust-proxmox-system-report-1.0.0-dev (= ${binary:Version}), > + librust-proxmox-system-report-1.0.0+default-dev (= ${binary:Version}) > +Description: Shared system report functionality - Rust source code > + Source code for Debianized Rust crate "proxmox-system-report" > diff --git a/proxmox-system-report/debian/copyright b/proxmox-system-report/debian/copyright > new file mode 100644 > index 00000000..01138fa0 > --- /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: 2026 Proxmox Server Solutions GmbH > +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 . > 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 " > + > +[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..9ae3e573 > --- /dev/null > +++ b/proxmox-system-report/src/lib.rs > @@ -0,0 +1,239 @@ > +//! 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 with static, compile time known arguments: > +/// `(command, [arg, ...])`. > +pub type StaticArgsCommandSpec = (&'static str, Vec<&'static str>); > + > +/// A command to run with arguments generated dynamically at runtime: > +/// `(command, [arg, ...])`. > +type DynamicArgsCommandSpec = (&'static str, Vec); > + > +/// 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::>().join("\n"); > + format!("$ `{exe} {}`\n```\n{output}\n```", args.join(" ")) > +} > + > +fn common_files() -> Vec { > + vec![( > + "General System Info", > + vec![ > + "/etc/hostname", > + "/etc/hosts", > + "/etc/network/interfaces", > + "/etc/apt/sources.list", > + "/etc/apt/sources.list.d/", > + "/proc/pressure/", > + ], > + )] > +} > + > +fn common_commands() -> Vec { > + 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 common_dynamic_commands() -> Vec { > + 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 common_function_calls() -> Vec { > + vec![("System Load & Uptime", get_top_processes)] > +} > + > +fn get_file_content(file: impl AsRef) -> 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) -> 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, > + project_commands: Vec, > + project_function_calls: Vec, > +) -> String { > + // We deliberately output the shared files before the project specific files > + // to preserve the legacy report ordering. > + let file_contents = common_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::>() > + .join("\n\n"); > + > + format!("### {group}\n\n{group_content}") > + }) > + .collect::>() > + .join("\n\n"); > + > + let static_command_outputs = project_commands > + .into_iter() > + .chain(common_commands()) > + .map(|(command, args)| get_command_output(command, &args)); > + > + let dynamic_command_outputs = common_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::>() > + .join("\n\n"); > + > + let function_outputs = project_function_calls > + .iter() > + .chain(&common_function_calls()) > + .map(|(desc, function)| { > + let output = function(); > + format!("#### {desc}\n{}\n", output.trim_end()) > + }) > + .collect::>() > + .join("\n\n"); > + > + format!( > + "## FILES\n\n{file_contents}\n## COMMANDS\n\n{command_outputs}\n## FUNCTIONS\n\n{function_outputs}\n" > + ) > +}