From: "Lukas Wagner" <l.wagner@proxmox.com>
To: "Shannon Sterz" <s.sterz@proxmox.com>,
"Lukas Wagner" <l.wagner@proxmox.com>
Cc: Proxmox Datacenter Manager development discussion
<pdm-devel@lists.proxmox.com>
Subject: Re: [pdm-devel] [PATCH datacenter-manager 1/4] server: add system report implementation
Date: Mon, 01 Dec 2025 16:06:39 +0100 [thread overview]
Message-ID: <DEMZ27KUUA1Z.AHLH4VJTYX9L@proxmox.com> (raw)
In-Reply-To: <DEMYTMRO9K2J.2U7GA3YN9UHSS@proxmox.com>
On Mon Dec 1, 2025 at 3:55 PM CET, Shannon Sterz wrote:
> On Mon Dec 1, 2025 at 1:58 PM CET, Lukas Wagner wrote:
>> This code was taken from PBS and then adapted for PDM. While it could
>> make sense to refactor and then share some of the helper functions with
>> PBS, the benefit is rather small (the helpers are relatively trivial)
>> and now is not the best time for it. The risk and potential consequences
>> of diverging implementations is small.
>>
>> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
>> ---
>> server/src/lib.rs | 1 +
>> server/src/report.rs | 195 +++++++++++++++++++++++++++++++++++++++++++
>> 2 files changed, 196 insertions(+)
>> create mode 100644 server/src/report.rs
>>
>> diff --git a/server/src/lib.rs b/server/src/lib.rs
>> index bd8660a7..5ed10d69 100644
>> --- a/server/src/lib.rs
>> +++ b/server/src/lib.rs
>> @@ -11,6 +11,7 @@ pub mod parallel_fetcher;
>> pub mod remote_cache;
>> pub mod remote_tasks;
>> pub mod remote_updates;
>> +pub mod report;
>> pub mod resource_cache;
>> pub mod task_utils;
>> pub mod views;
>> diff --git a/server/src/report.rs b/server/src/report.rs
>> new file mode 100644
>> index 00000000..247db4f1
>> --- /dev/null
>> +++ b/server/src/report.rs
>> @@ -0,0 +1,195 @@
>> +use std::fmt::Write;
>> +use std::path::Path;
>> +use std::process::Command;
>> +
>> +// 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>)> {
>> + 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![
>> + "/etc/proxmox-datacenter-manager/access/user.cfg",
>> + "/etc/proxmox-datacenter-manager/access/acl.cfg",
>> + ],
>> + ),
>> + (
>> + "Others",
>> + vec![
>> + "/etc/proxmox-datacenter-manager/node.cfg",
>> + "/etc/proxmox-datacenter-manager/views.cfg",
>
> hm the only thing here is that layout descriptions could become rather
> long and i'm not sure how useful they are for support reasons (filters
> more so). however, not too much of an issue imo.
>
>> + ],
>> + ),
>> + ]
>> +}
>> +
>> +fn commands() -> Vec<(&'static str, Vec<&'static str>)> {
>> + vec![
>> + // ("<command>", vec![<arg [, arg]>])
>> + ("date", vec!["-R"]),
>> + (
>> + "proxmox-datacenter-manager-admin",
>> + vec!["versions", "--verbose"],
>> + ),
>> + ("proxmox-datacenter-manager-admin", vec!["remote", "list"]),
>> + // FIXME: Does not exist yet.
>> + // ("proxmox-datacenter-manager-admin", vec!["subscription", "get"]),
>> + ("proxmox-boot-tool", vec!["status"]),
>> + ("df", vec!["-h"]),
>> + (
>> + "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"]),
>> + ("arcstat", vec![]),
>> + ]
>> +}
>> +
>> +// (description, function())
>> +type FunctionMapping = (&'static str, fn() -> String);
>> +
>> +fn function_calls() -> Vec<FunctionMapping> {
>> + vec![("System Load & Uptime", get_top_processes)]
>> +}
>
> a little confused why this gets its own category. `top` usually appears
> more toward the top of our reports, this puts it on the very bottom.
> also `get_top_processes` essentially also just calls a command, so not
> sure why this is separated out.
>
This was just taken as-is from PBS, see [1]. I think it's called as a
function rather than a command because we limit the output to the first
30 lines?
[1] https://git.proxmox.com/?p=proxmox-backup.git;a=blob;f=src/server/report.rs;h=546555fb97ba65ae8a748e64aaf5869b6fc7dd2c;hb=HEAD#l99
>> +
>> +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 command_outputs = commands()
>> + .iter()
>> + .map(|(command, args)| get_command_output(command, args))
>> + .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"
>> + )
>> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-12-01 15:06 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-12-01 12:58 [pdm-devel] [PATCH datacenter-manager 0/4] system report via admin cli or API Lukas Wagner
2025-12-01 12:58 ` [pdm-devel] [PATCH datacenter-manager 1/4] server: add system report implementation Lukas Wagner
2025-12-01 14:55 ` Shannon Sterz
2025-12-01 15:06 ` Lukas Wagner [this message]
2025-12-01 12:58 ` [pdm-devel] [PATCH datacenter-manager 2/4] api: add system report API Lukas Wagner
2025-12-01 12:58 ` [pdm-devel] [PATCH datacenter-manager 3/4] cli: admin: add 'report' command to generate a system report Lukas Wagner
2025-12-01 12:58 ` [pdm-devel] [PATCH datacenter-manager 4/4] pdm-client: add bindings for system report generation Lukas Wagner
2025-12-01 14:55 ` [pdm-devel] [PATCH datacenter-manager 0/4] system report via admin cli or API Shannon Sterz
2025-12-01 15:05 ` Stefan Hanreich
2025-12-01 15:18 ` Lukas Wagner
2025-12-01 15:34 ` [pdm-devel] superseded: " Lukas Wagner
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=DEMZ27KUUA1Z.AHLH4VJTYX9L@proxmox.com \
--to=l.wagner@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
--cc=s.sterz@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.