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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox