all lists on 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>,
	Lukas Wagner <l.wagner@proxmox.com>
Subject: [PATCH proxmox-datacenter-manager 3/3] report: use shared proxmox-system-report crate
Date: Tue, 14 Jul 2026 10:31:56 +0200	[thread overview]
Message-ID: <20260714083156.99794-4-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 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




WARNING: multiple messages have this Message-ID (diff)
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-datacenter-manager 3/3] report: use shared proxmox-system-report crate
Date: Tue, 14 Jul 2026 10:31:56 +0200	[thread overview]
Message-ID: <20260714083156.99794-4-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 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




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

Thread overview: 8+ 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-14  8:31 ` [PATCH proxmox-backup 2/3] report: use shared proxmox-system-report crate Erik Fastermann
2026-07-14  8:31   ` Erik Fastermann
2026-07-14  8:31 ` Erik Fastermann [this message]
2026-07-14  8:31   ` [PATCH proxmox-datacenter-manager 3/3] " 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-4-e.fastermann@proxmox.com \
    --to=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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal