public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Erik Fastermann <e.fastermann@proxmox.com>
To: pbs-devel@lists.proxmox.com, pdm-devel@lists.proxmox.com
Cc: Erik Fastermann <e.fastermann@proxmox.com>
Subject: [PATCH proxmox-backup 2/3] report: use shared proxmox-system-report crate
Date: Tue, 14 Jul 2026 10:31:55 +0200	[thread overview]
Message-ID: <20260714083156.99794-3-e.fastermann@proxmox.com> (raw)
In-Reply-To: <20260714083156.99794-1-e.fastermann@proxmox.com>

Move the common report logic to the proxmox-system-report crate and
keep only the PBS specific files, commands and the Datastores
function here.

User-visible output changes:
- df now also shows the filesystem type (df -h -T), matching PDM
- adds the IPv4 and IPv6 route tables (ip -4/-6 route show),
  previously PDM only

The section order (FILES, COMMANDS, FUNCTIONS) and all product
specific entries are unchanged.

Suggested-by: Lukas Wagner <l.wagner@proxmox.com>
Suggested-by: Christian Ebner <c.ebner@proxmox.com>
Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
---
 Cargo.toml           |   2 +
 src/server/report.rs | 213 +++----------------------------------------
 2 files changed, 15 insertions(+), 200 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index a625370cf..fb7904d38 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -94,6 +94,7 @@ proxmox-sortable-macro = "1"
 proxmox-subscription = { version = "1.0.2", features = [ "api-types" ] }
 proxmox-syslog-api = { version = "1.1", features = [ "impl" ] }
 proxmox-sys = "1"
+proxmox-system-report = "0.1"
 proxmox-systemd = "1.0.1"
 proxmox-tfa = { version = "6.0.3", features = [ "api", "api-types" ] }
 proxmox-time = "2"
@@ -252,6 +253,7 @@ proxmox-sortable-macro.workspace = true
 proxmox-subscription.workspace = true
 proxmox-syslog-api.workspace = true
 proxmox-sys = { workspace = true, features = [ "timer" ] }
+proxmox-system-report.workspace = true
 proxmox-systemd.workspace = true
 proxmox-tfa.workspace = true
 proxmox-time.workspace = true
diff --git a/src/server/report.rs b/src/server/report.rs
index 297718d2e..442a8573d 100644
--- a/src/server/report.rs
+++ b/src/server/report.rs
@@ -1,33 +1,7 @@
-use std::fmt::Write;
-use std::path::Path;
-use std::process::Command;
+use proxmox_system_report::{CommandSpec, FileGroup, FunctionMapping};
 
-use proxmox_network_api::NetworkInterfaceType;
-
-fn get_top_processes() -> String {
-    let (exe, args) = ("top", vec!["-b", "-c", "-w512", "-n", "1", "-o", "TIME"]);
-    let output = Command::new(exe).args(&args).output();
-    let output = match output {
-        Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
-        Err(err) => err.to_string(),
-    };
-    let output = output.lines().take(30).collect::<Vec<&str>>().join("\n");
-    format!("$ `{exe} {}`\n```\n{output}\n```", args.join(" "))
-}
-
-fn files() -> Vec<(&'static str, Vec<&'static str>)> {
+fn files() -> Vec<FileGroup> {
     vec![
-        (
-            "General System Info",
-            vec![
-                "/etc/hostname",
-                "/etc/hosts",
-                "/etc/network/interfaces",
-                "/etc/apt/sources.list",
-                "/etc/apt/sources.list.d/",
-                "/proc/pressure/",
-            ],
-        ),
         (
             "Datastores & Remotes",
             vec!["/etc/proxmox-backup/datastore.cfg"],
@@ -65,192 +39,31 @@ fn files() -> Vec<(&'static str, Vec<&'static str>)> {
     ]
 }
 
-fn commands() -> Vec<(&'static str, Vec<&'static str>)> {
+fn commands() -> Vec<CommandSpec> {
     vec![
-        //  ("<command>", vec![<arg [, arg]>])
         ("date", vec!["-R"]),
         ("proxmox-backup-manager", vec!["versions", "--verbose"]),
         ("proxmox-backup-manager", vec!["subscription", "get"]),
         ("proxmox-backup-manager", vec!["ldap", "list"]),
         ("proxmox-backup-manager", vec!["openid", "list"]),
-        ("proxmox-boot-tool", vec!["status"]),
-        ("df", vec!["-h"]),
-        (
-            "lsblk",
-            vec![
-                "--ascii",
-                "-M",
-                "-o",
-                "+HOTPLUG,ROTA,PHY-SEC,FSTYPE,MODEL,TRAN",
-            ],
-        ),
-        ("bash", vec!["-c", "ls -l /dev/disk/by-*/"]),
-        ("zpool", vec!["status"]),
-        ("zfs", vec!["list"]),
-        ("zarcstat", vec![]),
-        ("dmidecode", vec!["-t", "bios"]),
-        ("lscpu", vec![]),
-        ("lspci", vec!["-nnk"]),
-        ("ip", vec!["-details", "-statistics", "a"]),
     ]
 }
 
-fn dynamic_commands() -> Vec<(&'static str, Vec<String>)> {
-    let mut commands = Vec::new();
-
-    match proxmox_network_api::config() {
-        Ok((config, _)) => {
-            for (name, iface) in config.interfaces {
-                if iface.interface_type == NetworkInterfaceType::Eth {
-                    commands.push(("ethtool", vec![name]));
-                }
-            }
-        }
-        Err(err) => {
-            eprintln!("failed to query network interfaces: {err}");
-        }
-    }
-
-    commands
-}
-
-// (description, function())
-type FunctionMapping = (&'static str, fn() -> String);
-
 fn function_calls() -> Vec<FunctionMapping> {
-    vec![
-        ("Datastores", || {
-            let config = match pbs_config::datastore::config() {
-                Ok((config, _digest)) => config,
-                _ => return String::from("could not read datastore config"),
-            };
-
-            let mut list = Vec::new();
-            for store in config.sections.keys() {
-                list.push(store.as_str());
-            }
-            format!("```\n{}\n```", list.join(", "))
-        }),
-        ("System Load & Uptime", get_top_processes),
-    ]
-}
-
-fn get_file_content(file: impl AsRef<Path>) -> String {
-    use proxmox_sys::fs::file_read_optional_string;
-    let content = match file_read_optional_string(&file) {
-        Ok(Some(content)) => content,
-        Ok(None) => String::from("# file does not exist"),
-        Err(err) => err.to_string(),
-    };
-    let file_name = file.as_ref().display();
-    format!("`$ cat '{file_name}'`\n```\n{}\n```", content.trim_end())
-}
-
-fn get_directory_content(path: impl AsRef<Path>) -> String {
-    let read_dir_iter = match std::fs::read_dir(&path) {
-        Ok(iter) => iter,
-        Err(err) => {
-            return format!(
-                "`$ cat '{}*'`\n```\n# read dir failed - {err}\n```",
-                path.as_ref().display(),
-            );
-        }
-    };
-    let mut out = String::new();
-    let mut first = true;
-    for entry in read_dir_iter {
-        let entry = match entry {
-            Ok(entry) => entry,
-            Err(err) => {
-                let _ = writeln!(out, "error during read-dir - {err}");
-                continue;
-            }
+    vec![("Datastores", || {
+        let config = match pbs_config::datastore::config() {
+            Ok((config, _digest)) => config,
+            _ => return String::from("could not read datastore config"),
         };
-        let path = entry.path();
-        if path.is_file() {
-            if first {
-                let _ = writeln!(out, "{}", get_file_content(path));
-                first = false;
-            } else {
-                let _ = writeln!(out, "\n{}", get_file_content(path));
-            }
-        } else {
-            let _ = writeln!(out, "skipping sub-directory `{}`", path.display());
-        }
-    }
-    out
-}
 
-fn get_command_output(exe: &str, args: &Vec<&str>) -> String {
-    let output = Command::new(exe)
-        .env("PROXMOX_OUTPUT_NO_BORDER", "1")
-        .args(args)
-        .output();
-    let output = match output {
-        Ok(output) => {
-            let mut out = String::from_utf8_lossy(&output.stdout)
-                .trim_end()
-                .to_string();
-            let stderr = String::from_utf8_lossy(&output.stderr)
-                .trim_end()
-                .to_string();
-            if !stderr.is_empty() {
-                let _ = writeln!(out, "\n```\nSTDERR:\n```\n{stderr}");
-            }
-            out
+        let mut list = Vec::new();
+        for store in config.sections.keys() {
+            list.push(store.as_str());
         }
-        Err(err) => err.to_string(),
-    };
-    format!("$ `{exe} {}`\n```\n{output}\n```", args.join(" "))
+        format!("```\n{}\n```", list.join(", "))
+    })]
 }
 
 pub fn generate_report() -> String {
-    let file_contents = files()
-        .iter()
-        .map(|group| {
-            let (group, files) = group;
-            let group_content = files
-                .iter()
-                .map(|file_name| {
-                    let path = Path::new(file_name);
-                    if path.is_dir() {
-                        get_directory_content(path)
-                    } else {
-                        get_file_content(file_name)
-                    }
-                })
-                .collect::<Vec<String>>()
-                .join("\n\n");
-
-            format!("### {group}\n\n{group_content}")
-        })
-        .collect::<Vec<String>>()
-        .join("\n\n");
-
-    let static_command_outputs = commands()
-        .into_iter()
-        .map(|(command, args)| get_command_output(command, &args));
-
-    let dynamic_command_outputs = dynamic_commands().into_iter().map(|(command, args)| {
-        let args = args.iter().map(String::as_str).collect();
-        get_command_output(command, &args)
-    });
-
-    let command_outputs = static_command_outputs
-        .chain(dynamic_command_outputs)
-        .collect::<Vec<String>>()
-        .join("\n\n");
-
-    let function_outputs = function_calls()
-        .iter()
-        .map(|(desc, function)| {
-            let output = function();
-            format!("#### {desc}\n{}\n", output.trim_end())
-        })
-        .collect::<Vec<String>>()
-        .join("\n\n");
-
-    format!(
-        "## FILES\n\n{file_contents}\n## COMMANDS\n\n{command_outputs}\n## FUNCTIONS\n\n{function_outputs}\n"
-    )
+    proxmox_system_report::generate_report(files(), commands(), function_calls())
 }
-- 
2.47.3




  parent reply	other threads:[~2026-07-14  8:32 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-14  8:31 [PATCH proxmox{,-backup,-datacenter-manager} 0/3] factor system report into shared crate Erik Fastermann
2026-07-14  8:31 ` [PATCH proxmox 1/3] system-report: add crate for shared report generation Erik Fastermann
2026-07-14  8:31 ` Erik Fastermann [this message]
2026-07-14  8:31 ` [PATCH proxmox-datacenter-manager 3/3] report: use shared proxmox-system-report crate Erik Fastermann
2026-07-14  9:05 ` [PATCH proxmox{,-backup,-datacenter-manager} 0/3] factor system report into shared crate Erik Fastermann

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260714083156.99794-3-e.fastermann@proxmox.com \
    --to=e.fastermann@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal