[pdm-devel] [PATCH datacenter-manager 1/4] server: add system report implementation
Lukas Wagner
l.wagner at proxmox.com
Mon Dec 1 16:06:39 CET 2025
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 at 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"
>> + )
>> +}
More information about the pdm-devel
mailing list