all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs
@ 2025-07-26  1:05 Aaron Lauterer
  2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 1/3] create proxmox-rrd-migration-tool Aaron Lauterer
                   ` (33 more replies)
  0 siblings, 34 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:05 UTC (permalink / raw)
  To: pve-devel

This patch series does a few things. It expands the RRD format for nodes and
 VMs. For all types (nodes, VMs, storage) we adjust the aggregation to align
 them with the way they are done on the Backup Server. Therefore, we have new
 RRD defitions for all 3 types.

New values are added for nodes and VMs. In particular:

Nodes:
* memfree
* arcsize
* pressures:
  * cpu some
  * io some
  * io full
  * mem some
  * mem full

VMs:
* memhost (memory consumption of all processes in the guests cgroup, host view)
* pressures:
  * cpu some
  * cpu full
  * io some
  * io full
  * mem some
  * mem full

The change in RRD columns and aggregation means, that we need new RRD files. To
not lose old RRD data, we need to migrate the old RRD files to the ones with
the new schema. Some initial performance tests showed that migrating 10k VM
RRD files took ~2m40s single threaded. This is way to long to do it within the
pmxcfs itself. Therefore this will be a dedicated step:
The new `proxmox-rrd-migration-tool` migrates the RRD files to the new location
and aggregation schemas. It is run automatically by the postinst script of the
pve-manager.

This also means, that we need to handle the situation of new and old RRD
files and formats. Therefore we introduce new keys by which the metrics
are broadcast in a cluster. Up until now (pre PVE9), it is in the format of
'pve2-{type}/{resource id}'.
Having the version number this early in the string makes it tough to match
against newer ones, especially in the C code of the pmxcfs. To make it easier
in the future, we change the key format to 'pve-{type}-{version}/{resource id}'.
This way, we can fuzzy match against unknown 'pve-{type}-{version}' in the C
code too and handle those situations better.

The result is, that to avoid breaking changes, we are only allowed to add new
columns, but not modify or remove existing columns!


To avoid missing data and key errors in the journal, we already bumped 
changes to PVE 8 so it can handle the new format sent out by pvestatd in the
latest versions.

On the GUI side, we switch memory graphs to stacked area graphs and for VMs
we also have a dedicated line for the memory consumption as the host sees it.
Because the current memory view of a VM will switch to the internal guest view,
if we get detailed infos via the ballooning device.
To make those slightly more complicated graphs possible, we need to adapt
RRDChart.js in the widget-toolkit to allow for detailed overrides.

While we are at it, we can also fix bug #6068 (Node Search tab incorrect Host
memory usage %) by switching to memhost if available and one wrong if check.


As a side note, now that we got pressure graphs, we could start thinking about
dropping the server load and IO wait graphs. Those are not very specific and
mash many different metrics into a single one.


Release notes:
We should probably mention in the release notes, that due to the changed
aggregation settings, it is expected that the resulting RRD files might have
some data points that the originals didn't have. We observed that in some
situation we get could get a data point in one time step earlier than before.
This is most likely due to how RRD recalculates the aggregated data with the
different resolution.

In the pve8to9 checks, we now have a check that makes sure we do have enough
free space, as the new RRD files with the new columns and more detailed
aggeration steps, are quite a bit larger. We also check after install, if any
RRD files have not yet been migrated, which would warrant another manual run of
the migration tool.

Plans:
* add doc patches for the summary pages that explain the different graphs and
make the help button point to those sections

KNOWN ISSUES:
* on a live system, renaming the source RRD files to FILE.old doesn't seem to
work as expected and besides the renamed ones, new ones without the .old prefix
show up again. I suspect some interaction with rrdached and/or pmxcfs receiving
new data.

How to test:
1. have PVE8 nodes on the latest version (>= 8.4.4)
2. Upgrade the first node to PVE9/trixie and install all the other patches
    to see the automatic upgrade, pve-manager might need to be temporarily
    bumped to 9.0.0~12!
    build all the other repositories, copy the .deb files over and then ideally
    use something like the following to make shure that any dependency will be
    used from the deb files, and not the apt repositories.
    ```
    apt install ./*.deb --reinstall --allow-downgrades -y
    ```
3. you should see, if the pve-manager package calling the
proxmox-rrd-migration-tool


High level changes since:
v3:
* added check for pve8to9 for both situations, pre and post migration
* rebase and only send not yet applied patches
* incorporate suggested changes and improvement
* improve proxmox-rrd-migraton-tool
  * code style and refactoring of repetitive parts
  * rename processed files to FILE.old
  * tests
  * initial packaging
* drop info button tooltip patch. The concept was interesting, but would
introduce a new way to interact in just one place and doesn't work well on touch
devices.

v2:
* several bugfixes that I found, especially regarding pressure and memory
  collection for CTs and VMs
* add missing return property descriptions for pressures
* added all the GUI changes

v1:
* refactored the patches as they were a bit of a mess in v1, sorry for that
  now we have distinct patches for pve8 for both affected repos (cluster & manager)

RFC:
* drop membuffer and memcached in favor of already present memused and memavailable
* switch from pve9-{type} to pve-{type}-9.0 schema in all places
* add patch for PVE8 & 9 that handles different keys in live status to avoid
  question marks in the UI

proxmox-rrd-migration-tool:

Aaron Lauterer (3):
  create proxmox-rrd-migration-tool
  add first tests
  add debian packaging


cluster:

Aaron Lauterer (2):
  status: introduce new pve-{type}- rrd and metric format
  rrd: adapt to new RRD format with different aggregation windows

 src/PVE/RRD.pm      |  52 +++++++--
 src/pmxcfs/status.c | 261 +++++++++++++++++++++++++++++++++++++++-----
 2 files changed, 278 insertions(+), 35 deletions(-)


widget-toolkit:

Aaron Lauterer (4):
  rrdchart: allow to override the series object
  rrdchart: use reference for undo button
  rrdchard: set cursor pointer for legend
  rrdchart: add dummy listener for legend clicks

 src/panel/RRDChart.js | 61 ++++++++++++++++++++++++++++++++++---------
 1 file changed, 48 insertions(+), 13 deletions(-)


manager:

Aaron Lauterer (14):
  pvestatd: collect and distribute new pve-{type}-9.0 metrics
  api: nodes: rrd and rrddata add decade option and use new pve-node-9.0
    rrd files
  api2tools: extract_vm_status add new vm memhost column
  ui: rrdmodels: add new columns and update existing
  ui: node summary: use stacked memory graph with zfs arc
  ui: GuestStatusView: add memhost for VM guests
  ui: GuestSummary: memory switch to stacked and add hostmem
  ui: GuestSummary: remember visibility of host memory view
  ui: nodesummary: guestsummary: add tooltip info buttons
  ui: summaries: use titles for disk and network series
  fix #6068: ui: utils: calculate and render host memory usage correctly
  d/control: require proxmox-rrd-migration-tool >= 1.0.0
  d/postinst: run promox-rrd-migration-tool
  pve8to9: add checkfs for RRD migration

Folke Gleumes (1):
  ui: add pressure graphs to node and guest summary

 PVE/API2/Cluster.pm                   |   7 +
 PVE/API2/Nodes.pm                     |  16 +-
 PVE/API2Tools.pm                      |   3 +
 PVE/CLI/pve8to9.pm                    |  62 +++++
 PVE/Service/pvestatd.pm               | 342 +++++++++++++++++++-------
 debian/control                        |   1 +
 debian/postinst                       |   5 +
 www/manager6/Utils.js                 |   6 +
 www/manager6/data/ResourceStore.js    |   8 +
 www/manager6/data/model/RRDModels.js  |  44 +++-
 www/manager6/node/Summary.js          |  79 +++++-
 www/manager6/panel/GuestStatusView.js |  18 +-
 www/manager6/panel/GuestSummary.js    | 103 +++++++-
 13 files changed, 587 insertions(+), 107 deletions(-)


storage:

Aaron Lauterer (1):
  status: rrddata: use new pve-storage-9.0 rrd location if file is
    present

 src/PVE/API2/Storage/Status.pm | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)


qemu-server:

Aaron Lauterer (3):
  vmstatus: add memhost for host view of vm mem consumption
  vmstatus: switch mem stat to PSS of VM cgroup
  rrddata: use new pve-vm-9.0 rrd location if file is present

Folke Gleumes (1):
  metrics: add pressure to metrics

 src/PVE/API2/Qemu.pm  | 11 ++++----
 src/PVE/QemuServer.pm | 65 +++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 69 insertions(+), 7 deletions(-)


container:

Aaron Lauterer (1):
  rrddata: use new pve-vm-9.0 rrd location if file is present

Folke Gleumes (1):
  metrics: add pressures to metrics

 src/PVE/API2/LXC.pm | 11 ++++++-----
 src/PVE/LXC.pm      | 34 ++++++++++++++++++++++++++++++++++
 2 files changed, 40 insertions(+), 5 deletions(-)


Summary over all repositories:
  21 files changed, 1026 insertions(+), 172 deletions(-)

-- 
Generated by git-murpp 0.8.1


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH proxmox-rrd-migration-tool v4 1/3] create proxmox-rrd-migration-tool
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
@ 2025-07-26  1:05 ` Aaron Lauterer
  2025-07-28 14:25   ` Lukas Wagner
  2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 2/3] add first tests Aaron Lauterer
                   ` (32 subsequent siblings)
  33 siblings, 1 reply; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:05 UTC (permalink / raw)
  To: pve-devel

This tool is intended to migrate the Proxmox VE (PVE) RRD data files to
the new schema.

Up until PVE8 the schema has been the same for a long time. With PVE9 we
introduced new columns to guests (vm) and nodes. We also switched all
types (vm, node, storate) to the same aggregation schemas as we do it in
PBS.
The result of both are a much finer resolution for long time spans, but
also larger RRD files.

* node: 79K -> 1.4M
* vm: 66K -> 1.3m
* storage: 14K -> 156K

The old directories for VMs used to be in `/var/lib/rrdcached/db/` with
the following sub directories:

* nodes: `pve2-node`
* guests (VM/CT): `pve2-vm`
* storage: `pve2-storage`

With this change we also introduce a new key schema, that makes it
easier in the future to introduce new ones. Instead of the
`pve{version}-{type}` we are switching to `pve-{type}-{version}`.

This enables us to add new columns with a new version, without breaking
nodes that are not yet updated. We are NOT allowed to remove or re-use
existing columns. That would be a breaking change.
We are currently at version 9.0. But in the future, if needed, this tool
can be adapted to do other migrations too.
For example, {old, 9.0} -> 9.2, should that be necessary.

The actual migration is handled by `librrd` to which we pass the path to
the old and new files, and the new RRD definitions. The `rrd_create_r2`
call then does the hard work of migrating and converting exisiting data
into the new file and aggregation schema.

This can take some time. Quick tests on a Ryzen 7900X with the following
files:
* 1 node RRD file
* 10k vm RRD files
* 1 storage RRD file

showed the folling results:

* 1 thread:  179.61s user 14.82s system 100% cpu 3:14.17 total
* 4 threads: 187.57s user 16.98s system 399% cpu 51.198 total

This is why we do not migrate inline, but have it as a separate step
during package upgrades.

Behavior: By default nothing will be changed and a dry or test run will
happen.
Only if the `--migrate` parameter is added will the actual migration be
done.

For each found RRD source file, the tool checks if a matching target
file already exists. By default, those will be skipped to not overwrite
target files that might already store newer data.
With the `--force` parameter this can be changed.

That means, one can run the tool multiple times (without --force) and it
will pick up where it might have left off. For example it the migration
was interrupted for some reason.

Once a source file has been processed it will be renamed with the `.old`
appendix. It will be excluded from future runs as we check for files
without an extension.

The tool has some simple heuristic to determine how many threads should
be used. Be default the range is between 1 to 4 threads. But the
`--threads` parameter allows a manual override.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
 .cargo/config.toml      |   5 +
 .gitignore              |   9 +
 Cargo.toml              |  20 ++
 build.rs                |  29 ++
 src/lib.rs              |   5 +
 src/main.rs             | 567 ++++++++++++++++++++++++++++++++++++++++
 src/parallel_handler.rs | 160 ++++++++++++
 wrapper.h               |   1 +
 8 files changed, 796 insertions(+)
 create mode 100644 .cargo/config.toml
 create mode 100644 .gitignore
 create mode 100644 Cargo.toml
 create mode 100644 build.rs
 create mode 100644 src/lib.rs
 create mode 100644 src/main.rs
 create mode 100644 src/parallel_handler.rs
 create mode 100644 wrapper.h

diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..3b5b6e4
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,5 @@
+[source]
+[source.debian-packages]
+directory = "/usr/share/cargo/registry"
+[source.crates-io]
+replace-with = "debian-packages"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..06ac1a1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+*.build
+*.buildinfo
+*.changes
+*.deb
+*.dsc
+*.tar*
+target/
+/Cargo.lock
+/proxmox-rrd-migration-tool-[0-9]*/
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..d3523f3
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "proxmox_rrd_migration_8-9"
+version = "0.1.0"
+edition = "2021"
+authors = [
+    "Aaron Lauterer <a.lauterer@proxmox.com>",
+    "Proxmox Support Team <support@proxmox.com>",
+]
+license = "AGPL-3"
+homepage = "https://www.proxmox.com"
+
+[dependencies]
+anyhow = "1.0.86"
+pico-args = "0.5.0"
+proxmox-async = "0.4"
+crossbeam-channel = "0.5"
+
+[build-dependencies]
+bindgen = "0.66.1"
+pkg-config = "0.3"
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..56d07cc
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,29 @@
+use std::env;
+use std::path::PathBuf;
+
+fn main() {
+    println!("cargo:rustc-link-lib=rrd");
+
+    println!("cargo:rerun-if-changed=wrapper.h");
+    // The bindgen::Builder is the main entry point
+    // to bindgen, and lets you build up options for
+    // the resulting bindings.
+
+    let bindings = bindgen::Builder::default()
+        // The input header we would like to generate
+        // bindings for.
+        .header("wrapper.h")
+        // Tell cargo to invalidate the built crate whenever any of the
+        // included header files changed.
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
+        // Finish the builder and generate the bindings.
+        .generate()
+        // Unwrap the Result and panic on failure.
+        .expect("Unable to generate bindings");
+
+    // Write the bindings to the $OUT_DIR/bindings.rs file.
+    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+    bindings
+        .write_to_file(out_path.join("bindings.rs"))
+        .expect("Couldn't write bindings!");
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..a38a13a
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,5 @@
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..5e6418c
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,567 @@
+use anyhow::{bail, Error, Result};
+use std::{
+    ffi::{CStr, CString, OsString},
+    fs,
+    os::unix::{ffi::OsStrExt, fs::PermissionsExt},
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+
+use proxmox_rrd_migration_tool::{rrd_clear_error, rrd_create_r2, rrd_get_context, rrd_get_error};
+
+use crate::parallel_handler::ParallelHandler;
+
+pub mod parallel_handler;
+
+const BASE_DIR: &str = "/var/lib/rrdcached/db";
+const SOURCE_SUBDIR_NODE: &str = "pve2-node";
+const SOURCE_SUBDIR_GUEST: &str = "pve2-vm";
+const SOURCE_SUBDIR_STORAGE: &str = "pve2-storage";
+const TARGET_SUBDIR_NODE: &str = "pve-node-9.0";
+const TARGET_SUBDIR_GUEST: &str = "pve-vm-9.0";
+const TARGET_SUBDIR_STORAGE: &str = "pve-storage-9.0";
+const RESOURCE_BASE_DIR: &str = "/etc/pve";
+const MAX_THREADS: usize = 4;
+const RRD_STEP_SIZE: usize = 60;
+
+type File = (CString, OsString);
+
+// RRAs are defined in the following way:
+//
+// RRA:CF:xff:step:rows
+// CF: AVERAGE or MAX
+// xff: 0.5
+// steps: stepsize is defined on rrd file creation! example: with 60 seconds step size:
+//	e.g. 1 => 60 sec, 30 => 1800 seconds or 30 min
+// rows: how many aggregated rows are kept, as in how far back in time we store data
+//
+// how many seconds are aggregated per RRA: steps * stepsize * rows
+// how many hours are aggregated per RRA: steps * stepsize * rows / 3600
+// how many days are aggregated per RRA: steps * stepsize * rows / 3600 / 24
+// https://oss.oetiker.ch/rrdtool/tut/rrd-beginners.en.html#Understanding_by_an_example
+
+const RRD_VM_DEF: [&CStr; 25] = [
+    c"DS:maxcpu:GAUGE:120:0:U",
+    c"DS:cpu:GAUGE:120:0:U",
+    c"DS:maxmem:GAUGE:120:0:U",
+    c"DS:mem:GAUGE:120:0:U",
+    c"DS:maxdisk:GAUGE:120:0:U",
+    c"DS:disk:GAUGE:120:0:U",
+    c"DS:netin:DERIVE:120:0:U",
+    c"DS:netout:DERIVE:120:0:U",
+    c"DS:diskread:DERIVE:120:0:U",
+    c"DS:diskwrite:DERIVE:120:0:U",
+    c"DS:memhost:GAUGE:120:0:U",
+    c"DS:pressurecpusome:GAUGE:120:0:U",
+    c"DS:pressurecpufull:GAUGE:120:0:U",
+    c"DS:pressureiosome:GAUGE:120:0:U",
+    c"DS:pressureiofull:GAUGE:120:0:U",
+    c"DS:pressurememorysome:GAUGE:120:0:U",
+    c"DS:pressurememoryfull:GAUGE:120:0:U",
+    c"RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
+    c"RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
+    c"RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
+    c"RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
+    c"RRA:MAX:0.5:1:1440",        // 1 min * 1440 => 1 day
+    c"RRA:MAX:0.5:30:1440",       // 30 min * 1440 => 30 day
+    c"RRA:MAX:0.5:360:1440",      // 6 hours * 1440 => 360 day ~1 year
+    c"RRA:MAX:0.5:10080:570",     // 1 week * 570 => ~10 years
+];
+
+const RRD_NODE_DEF: [&CStr; 27] = [
+    c"DS:loadavg:GAUGE:120:0:U",
+    c"DS:maxcpu:GAUGE:120:0:U",
+    c"DS:cpu:GAUGE:120:0:U",
+    c"DS:iowait:GAUGE:120:0:U",
+    c"DS:memtotal:GAUGE:120:0:U",
+    c"DS:memused:GAUGE:120:0:U",
+    c"DS:swaptotal:GAUGE:120:0:U",
+    c"DS:swapused:GAUGE:120:0:U",
+    c"DS:roottotal:GAUGE:120:0:U",
+    c"DS:rootused:GAUGE:120:0:U",
+    c"DS:netin:DERIVE:120:0:U",
+    c"DS:netout:DERIVE:120:0:U",
+    c"DS:memfree:GAUGE:120:0:U",
+    c"DS:arcsize:GAUGE:120:0:U",
+    c"DS:pressurecpusome:GAUGE:120:0:U",
+    c"DS:pressureiosome:GAUGE:120:0:U",
+    c"DS:pressureiofull:GAUGE:120:0:U",
+    c"DS:pressurememorysome:GAUGE:120:0:U",
+    c"DS:pressurememoryfull:GAUGE:120:0:U",
+    c"RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
+    c"RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
+    c"RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
+    c"RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
+    c"RRA:MAX:0.5:1:1440",        // 1 min * 1440 => 1 day
+    c"RRA:MAX:0.5:30:1440",       // 30 min * 1440 => 30 day
+    c"RRA:MAX:0.5:360:1440",      // 6 hours * 1440 => 360 day ~1 year
+    c"RRA:MAX:0.5:10080:570",     // 1 week * 570 => ~10 years
+];
+
+const RRD_STORAGE_DEF: [&CStr; 10] = [
+    c"DS:total:GAUGE:120:0:U",
+    c"DS:used:GAUGE:120:0:U",
+    c"RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
+    c"RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
+    c"RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
+    c"RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
+    c"RRA:MAX:0.5:1:1440",        // 1 min * 1440 => 1 day
+    c"RRA:MAX:0.5:30:1440",       // 30 min * 1440 => 30 day
+    c"RRA:MAX:0.5:360:1440",      // 6 hours * 1440 => 360 day ~1 year
+    c"RRA:MAX:0.5:10080:570",     // 1 week * 570 => ~10 years
+];
+
+const HELP: &str = "\
+proxmox-rrd-migration tool
+
+Migrates existing RRD graph data to the new format.
+
+Use this only in the process of upgrading from Proxmox VE 8 to 9 according to the upgrade guide!
+
+USAGE:
+    proxmox-rrd-migration [OPTIONS]
+
+    FLAGS:
+        -h, --help              Prints this help information
+
+    OPTIONS:
+        --migrate               Start the migration. Without it, only a dry run will be done.
+
+        --force                 Migrate, even if the target already exists.
+                                This will overwrite any migrated RRD files!
+
+        --threads THREADS       Number of paralell threads.
+
+        --source <SOURCE DIR>   Source base directory. Mainly for tests!
+                                Default: /var/lib/rrdcached/db
+
+        --target <TARGET DIR>   Target base directory. Mainly for tests!
+                                Default: /var/lib/rrdcached/db
+
+        --resources <DIR>       Directory that contains .vmlist and .member files. Mainly for tests!
+                                Default: /etc/pve
+
+";
+
+#[derive(Debug)]
+struct Args {
+    migrate: bool,
+    force: bool,
+    threads: Option<usize>,
+    source: Option<String>,
+    target: Option<String>,
+    resources: Option<String>,
+}
+
+fn parse_args() -> Result<Args, Error> {
+    let mut pargs = pico_args::Arguments::from_env();
+
+    // Help has a higher priority and should be handled separately.
+    if pargs.contains(["-h", "--help"]) {
+        print!("{}", HELP);
+        std::process::exit(0);
+    }
+
+    let mut args = Args {
+        migrate: false,
+        threads: pargs
+            .opt_value_from_str("--threads")
+            .expect("Could not parse --threads parameter"),
+        force: false,
+        source: pargs
+            .opt_value_from_str("--source")
+            .expect("Could not parse --source parameter"),
+        target: pargs
+            .opt_value_from_str("--target")
+            .expect("Could not parse --target parameter"),
+        resources: pargs
+            .opt_value_from_str("--resources")
+            .expect("Could not parse --resources parameter"),
+    };
+
+    if pargs.contains("--migrate") {
+        args.migrate = true;
+    }
+    if pargs.contains("--force") {
+        args.force = true;
+    }
+
+    // It's up to the caller what to do with the remaining arguments.
+    let remaining = pargs.finish();
+    if !remaining.is_empty() {
+        bail!(format!("Warning: unused arguments left: {:?}", remaining));
+    }
+
+    Ok(args)
+}
+
+fn main() {
+    let args = match parse_args() {
+        Ok(v) => v,
+        Err(e) => {
+            eprintln!("Error: {}.", e);
+            std::process::exit(1);
+        }
+    };
+
+    let source_base_dir = match args.source {
+        Some(ref v) => v.as_str(),
+        None => BASE_DIR,
+    };
+
+    let target_base_dir = match args.target {
+        Some(ref v) => v.as_str(),
+        None => BASE_DIR,
+    };
+
+    let resource_base_dir = match args.resources {
+        Some(ref v) => v.as_str(),
+        None => RESOURCE_BASE_DIR,
+    };
+
+    let source_dir_guests: PathBuf = [source_base_dir, SOURCE_SUBDIR_GUEST].iter().collect();
+    let target_dir_guests: PathBuf = [target_base_dir, TARGET_SUBDIR_GUEST].iter().collect();
+    let source_dir_nodes: PathBuf = [source_base_dir, SOURCE_SUBDIR_NODE].iter().collect();
+    let target_dir_nodes: PathBuf = [target_base_dir, TARGET_SUBDIR_NODE].iter().collect();
+    let source_dir_storage: PathBuf = [source_base_dir, SOURCE_SUBDIR_STORAGE].iter().collect();
+    let target_dir_storage: PathBuf = [target_base_dir, TARGET_SUBDIR_STORAGE].iter().collect();
+
+    if !args.migrate {
+        println!("DRYRUN! Use the --migrate parameter to start the migration.");
+    }
+    if args.force {
+        println!("Force mode! Will overwrite existing target RRD files!");
+    }
+
+    if let Err(e) = migrate_nodes(
+        source_dir_nodes,
+        target_dir_nodes,
+        resource_base_dir,
+        args.migrate,
+        args.force,
+    ) {
+        eprintln!("Error migrating nodes: {}", e);
+        std::process::exit(1);
+    }
+    if let Err(e) = migrate_storage(
+        source_dir_storage,
+        target_dir_storage,
+        args.migrate,
+        args.force,
+    ) {
+        eprintln!("Error migrating storage: {}", e);
+        std::process::exit(1);
+    }
+    if let Err(e) = migrate_guests(
+        source_dir_guests,
+        target_dir_guests,
+        resource_base_dir,
+        set_threads(&args),
+        args.migrate,
+        args.force,
+    ) {
+        eprintln!("Error migrating guests: {}", e);
+        std::process::exit(1);
+    }
+}
+
+/// Set number of threads
+///
+/// Either a fixed parameter or determining a range between 1 to 4 threads
+///  based on the number of CPU cores available in the system.
+fn set_threads(args: &Args) -> usize {
+    if let Some(threads) = args.threads {
+        return threads;
+    }
+
+    // check for a way to get physical cores and not threads?
+    let cpus: usize = String::from_utf8_lossy(
+        std::process::Command::new("nproc")
+            .output()
+            .expect("Error running nproc")
+            .stdout
+            .as_slice()
+            .trim_ascii(),
+    )
+    .parse::<usize>()
+    .expect("Could not parse nproc output");
+
+    if cpus < 32 {
+        let threads = cpus / 8;
+        if threads == 0 {
+            return 1;
+        }
+        return threads;
+    }
+    MAX_THREADS
+}
+
+/// Check if a VMID is currently configured
+fn resource_present(path: &str, resource: &str) -> Result<bool> {
+    let resourcelist = fs::read_to_string(path)?;
+    Ok(resourcelist.contains(format!("\"{resource}\"").as_str()))
+}
+
+/// Rename file to old, when migrated or resource not present at all -> old RRD file
+fn mv_old(file: &str) -> Result<()> {
+    let old = format!("{}.old", file);
+    fs::rename(file, old)?;
+    Ok(())
+}
+
+/// Colllect all RRD files in the provided directory
+fn collect_rrd_files(location: &PathBuf) -> Result<Vec<(CString, OsString)>> {
+    let mut files: Vec<(CString, OsString)> = Vec::new();
+
+    fs::read_dir(location)?
+        .filter(|f| f.is_ok())
+        .map(|f| f.unwrap().path())
+        .filter(|f| f.is_file() && f.extension().is_none())
+        .for_each(|file| {
+            let path = CString::new(file.as_path().as_os_str().as_bytes())
+                .expect("Could not convert path to CString.");
+            let fname = file
+                .file_name()
+                .map(|v| v.to_os_string())
+                .expect("Could not convert fname to OsString.");
+            files.push((path, fname))
+        });
+    Ok(files)
+}
+
+/// Does the actual migration for the given file
+fn do_rrd_migration(
+    file: File,
+    target_location: &Path,
+    rrd_def: &[&CStr],
+    migrate: bool,
+    force: bool,
+) -> Result<()> {
+    if !migrate {
+        println!("would migrate but in dry run mode");
+    }
+
+    let resource = file.1;
+    let mut target_path = target_location.to_path_buf();
+    target_path.push(resource);
+
+    if target_path.exists() && !force {
+        println!(
+            "already migrated, use --force to overwrite target file: {}",
+            target_path.display()
+        );
+    }
+
+    if !migrate || (target_path.exists() && !force) {
+        bail!("skipping");
+    }
+
+    let mut source: [*const i8; 2] = [std::ptr::null(); 2];
+    source[0] = file.0.as_ptr();
+
+    let target_path = CString::new(target_path.to_str().unwrap()).unwrap();
+
+    unsafe {
+        rrd_get_context();
+        rrd_clear_error();
+        let res = rrd_create_r2(
+            target_path.as_ptr(),
+            RRD_STEP_SIZE as u64,
+            0,
+            0,
+            source.as_mut_ptr(),
+            std::ptr::null(),
+            rrd_def.len() as i32,
+            rrd_def
+                .iter()
+                .map(|v| v.as_ptr())
+                .collect::<Vec<_>>()
+                .as_mut_ptr(),
+        );
+        if res != 0 {
+            bail!(
+                "RRD create Error: {}",
+                CStr::from_ptr(rrd_get_error()).to_string_lossy()
+            );
+        }
+    }
+    Ok(())
+}
+
+/// Migrate guest RRD files
+///
+/// In parallel to speed up the process as most time is spent on converting the
+/// data to the new format.
+fn migrate_guests(
+    source_dir_guests: PathBuf,
+    target_dir_guests: PathBuf,
+    resources: &str,
+    threads: usize,
+    migrate: bool,
+    force: bool,
+) -> Result<(), Error> {
+    println!("Migrating RRD data for guests…");
+    println!("Using {} thread(s)", threads);
+
+    let guest_source_files = collect_rrd_files(&source_dir_guests)?;
+
+    if !target_dir_guests.exists() && migrate {
+        println!("Creating new directory: '{}'", target_dir_guests.display());
+        std::fs::create_dir(&target_dir_guests)?;
+    }
+
+    let total_guests = guest_source_files.len();
+    let guests = Arc::new(std::sync::atomic::AtomicUsize::new(0));
+    let guests2 = guests.clone();
+    let start_time = std::time::SystemTime::now();
+
+    let migration_pool = ParallelHandler::new(
+        "guest rrd migration",
+        threads,
+        move |file: (CString, OsString)| {
+            let full_path = file.0.clone().into_string().unwrap();
+
+            if let Ok(()) = do_rrd_migration(
+                file,
+                &target_dir_guests,
+                RRD_VM_DEF.as_slice(),
+                migrate,
+                force,
+            ) {
+                mv_old(full_path.as_str())?;
+                let current_guests = guests2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
+                if current_guests > 0 && current_guests % 200 == 0 {
+                    println!("Migrated {} of {} guests", current_guests, total_guests);
+                }
+            }
+            Ok(())
+        },
+    );
+    let migration_channel = migration_pool.channel();
+
+    for file in guest_source_files {
+        let node = file.1.clone().into_string().unwrap();
+        if !resource_present(format!("{resources}/.vmlist").as_str(), node.as_str())? {
+            println!("VMID: '{node}' not present. Skip and mark as old.");
+            mv_old(format!("{}", file.0.to_string_lossy()).as_str())?;
+        }
+        let migration_channel = migration_channel.clone();
+        migration_channel.send(file)?;
+    }
+
+    drop(migration_channel);
+    migration_pool.complete()?;
+
+    let elapsed = start_time.elapsed()?.as_secs_f64();
+    let guests = guests.load(std::sync::atomic::Ordering::SeqCst);
+    println!("Migrated {} guests", guests);
+    println!("It took {:.2}s", elapsed);
+
+    Ok(())
+}
+
+/// Migrate node RRD files
+///
+/// In serial as the number of nodes will not be high.
+fn migrate_nodes(
+    source_dir_nodes: PathBuf,
+    target_dir_nodes: PathBuf,
+    resources: &str,
+    migrate: bool,
+    force: bool,
+) -> Result<(), Error> {
+    println!("Migrating RRD data for nodes…");
+
+    if !target_dir_nodes.exists() && migrate {
+        println!("Creating new directory: '{}'", target_dir_nodes.display());
+        std::fs::create_dir(&target_dir_nodes)?;
+    }
+
+    let node_source_files = collect_rrd_files(&source_dir_nodes)?;
+
+    for file in node_source_files {
+        let node = file.1.clone().into_string().unwrap();
+        let full_path = file.0.clone().into_string().unwrap();
+        println!("Node: '{node}'");
+        if !resource_present(format!("{resources}/.members").as_str(), node.as_str())? {
+            println!("Node: '{node}' not present. Skip and mark as old.");
+            mv_old(format!("{}/{}", file.0.to_string_lossy(), node).as_str())?;
+        }
+        if let Ok(()) = do_rrd_migration(
+            file,
+            &target_dir_nodes,
+            RRD_NODE_DEF.as_slice(),
+            migrate,
+            force,
+        ) {
+            mv_old(full_path.as_str())?;
+        }
+    }
+    println!("Migrated all nodes");
+
+    Ok(())
+}
+
+/// Migrate storage RRD files
+///
+/// In serial as the number of storage will not be that high.
+fn migrate_storage(
+    source_dir_storage: PathBuf,
+    target_dir_storage: PathBuf,
+    migrate: bool,
+    force: bool,
+) -> Result<(), Error> {
+    println!("Migrating RRD data for storages…");
+
+    if !target_dir_storage.exists() && migrate {
+        println!("Creating new directory: '{}'", target_dir_storage.display());
+        std::fs::create_dir(&target_dir_storage)?;
+    }
+
+    // storage has another layer of directories per node over which we need to iterate
+    fs::read_dir(&source_dir_storage)?
+        .filter(|f| f.is_ok())
+        .map(|f| f.unwrap().path())
+        .filter(|f| f.is_dir())
+        .try_for_each(|node| {
+            let mut source_storage_subdir = source_dir_storage.clone();
+            source_storage_subdir.push(node.file_name().unwrap());
+
+            let mut target_storage_subdir = target_dir_storage.clone();
+            target_storage_subdir.push(node.file_name().unwrap());
+
+            if !target_storage_subdir.exists() && migrate {
+                fs::create_dir(target_storage_subdir.as_path())?;
+                let metadata = target_storage_subdir.metadata()?;
+                let mut permissions = metadata.permissions();
+                permissions.set_mode(0o755);
+            }
+
+            let storage_source_files = collect_rrd_files(&source_storage_subdir)?;
+
+            for file in storage_source_files {
+                println!(
+                    "Storage: '{}/{}'",
+                    node.file_name()
+                        .expect("no file name present")
+                        .to_string_lossy(),
+                    PathBuf::from(file.1.clone()).display()
+                );
+
+                let full_path = file.0.clone().into_string().unwrap();
+                if let Ok(()) = do_rrd_migration(
+                    file,
+                    &target_storage_subdir,
+                    RRD_STORAGE_DEF.as_slice(),
+                    migrate,
+                    force,
+                ) {
+                    mv_old(full_path.as_str())?;
+                }
+            }
+            Ok::<(), Error>(())
+        })?;
+    println!("Migrated all storages");
+
+    Ok(())
+}
diff --git a/src/parallel_handler.rs b/src/parallel_handler.rs
new file mode 100644
index 0000000..d8ee3c7
--- /dev/null
+++ b/src/parallel_handler.rs
@@ -0,0 +1,160 @@
+//! A thread pool which run a closure in parallel.
+
+use std::sync::{Arc, Mutex};
+use std::thread::JoinHandle;
+
+use anyhow::{bail, format_err, Error};
+use crossbeam_channel::{bounded, Sender};
+
+/// A handle to send data to the worker thread (implements clone)
+pub struct SendHandle<I> {
+    input: Sender<I>,
+    abort: Arc<Mutex<Option<String>>>,
+}
+
+/// Returns the first error happened, if any
+pub fn check_abort(abort: &Mutex<Option<String>>) -> Result<(), Error> {
+    let guard = abort.lock().unwrap();
+    if let Some(err_msg) = &*guard {
+        return Err(format_err!("{}", err_msg));
+    }
+    Ok(())
+}
+
+impl<I: Send> SendHandle<I> {
+    /// Send data to the worker threads
+    pub fn send(&self, input: I) -> Result<(), Error> {
+        check_abort(&self.abort)?;
+        match self.input.send(input) {
+            Ok(()) => Ok(()),
+            Err(_) => bail!("send failed - channel closed"),
+        }
+    }
+}
+
+/// A thread pool which run the supplied closure
+///
+/// The send command sends data to the worker threads. If one handler
+/// returns an error, we mark the channel as failed and it is no
+/// longer possible to send data.
+///
+/// When done, the 'complete()' method needs to be called to check for
+/// outstanding errors.
+pub struct ParallelHandler<I> {
+    handles: Vec<JoinHandle<()>>,
+    name: String,
+    input: Option<SendHandle<I>>,
+}
+
+impl<I> Clone for SendHandle<I> {
+    fn clone(&self) -> Self {
+        Self {
+            input: self.input.clone(),
+            abort: Arc::clone(&self.abort),
+        }
+    }
+}
+
+impl<I: Send + 'static> ParallelHandler<I> {
+    /// Create a new thread pool, each thread processing incoming data
+    /// with 'handler_fn'.
+    pub fn new<F>(name: &str, threads: usize, handler_fn: F) -> Self
+    where
+        F: Fn(I) -> Result<(), Error> + Send + Clone + 'static,
+    {
+        let mut handles = Vec::new();
+        let (input_tx, input_rx) = bounded::<I>(threads);
+
+        let abort = Arc::new(Mutex::new(None));
+
+        for i in 0..threads {
+            let input_rx = input_rx.clone();
+            let abort = Arc::clone(&abort);
+            let handler_fn = handler_fn.clone();
+
+            handles.push(
+                std::thread::Builder::new()
+                    .name(format!("{} ({})", name, i))
+                    .spawn(move || loop {
+                        let data = match input_rx.recv() {
+                            Ok(data) => data,
+                            Err(_) => return,
+                        };
+                        if let Err(err) = (handler_fn)(data) {
+                            let mut guard = abort.lock().unwrap();
+                            if guard.is_none() {
+                                *guard = Some(err.to_string());
+                            }
+                        }
+                    })
+                    .unwrap(),
+            );
+        }
+        Self {
+            handles,
+            name: name.to_string(),
+            input: Some(SendHandle {
+                input: input_tx,
+                abort,
+            }),
+        }
+    }
+
+    /// Returns a cloneable channel to send data to the worker threads
+    pub fn channel(&self) -> SendHandle<I> {
+        self.input.as_ref().unwrap().clone()
+    }
+
+    /// Send data to the worker threads
+    pub fn send(&self, input: I) -> Result<(), Error> {
+        self.input.as_ref().unwrap().send(input)?;
+        Ok(())
+    }
+
+    /// Wait for worker threads to complete and check for errors
+    pub fn complete(mut self) -> Result<(), Error> {
+        let input = self.input.take().unwrap();
+        let abort = Arc::clone(&input.abort);
+        check_abort(&abort)?;
+        drop(input);
+
+        let msg_list = self.join_threads();
+
+        // an error might be encountered while waiting for the join
+        check_abort(&abort)?;
+
+        if msg_list.is_empty() {
+            return Ok(());
+        }
+        Err(format_err!("{}", msg_list.join("\n")))
+    }
+
+    fn join_threads(&mut self) -> Vec<String> {
+        let mut msg_list = Vec::new();
+
+        let mut i = 0;
+        while let Some(handle) = self.handles.pop() {
+            if let Err(panic) = handle.join() {
+                if let Some(panic_msg) = panic.downcast_ref::<&str>() {
+                    msg_list.push(format!("thread {} ({i}) panicked: {panic_msg}", self.name));
+                } else if let Some(panic_msg) = panic.downcast_ref::<String>() {
+                    msg_list.push(format!("thread {} ({i}) panicked: {panic_msg}", self.name));
+                } else {
+                    msg_list.push(format!("thread {} ({i}) panicked", self.name));
+                }
+            }
+            i += 1;
+        }
+        msg_list
+    }
+}
+
+// Note: We make sure that all threads will be joined
+impl<I> Drop for ParallelHandler<I> {
+    fn drop(&mut self) {
+        drop(self.input.take());
+        while let Some(handle) = self.handles.pop() {
+            let _ = handle.join();
+        }
+    }
+}
diff --git a/wrapper.h b/wrapper.h
new file mode 100644
index 0000000..64d0aa6
--- /dev/null
+++ b/wrapper.h
@@ -0,0 +1 @@
+#include <rrd.h>
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH proxmox-rrd-migration-tool v4 2/3] add first tests
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
  2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 1/3] create proxmox-rrd-migration-tool Aaron Lauterer
@ 2025-07-26  1:05 ` Aaron Lauterer
  2025-07-28 14:52   ` Lukas Wagner
  2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 3/3] add debian packaging Aaron Lauterer
                   ` (31 subsequent siblings)
  33 siblings, 1 reply; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:05 UTC (permalink / raw)
  To: pve-devel

they are not pretty, but we now can test the following:

* resulting RRD file matches the expected rrdinfo output
  By running the resulting binary within 'faketime'
  -> had to filter out some lines that change with each iteration
* .old files are ignored
* processed files are renamed to have the .old appendix
* that a follow up run won't find anything to migrate
* that an RRD file for a VM that was created during the migration will
  be migrated in a second run

We also set RUST_TEST_THREADS to 1 in .cargo/config.toml as they
currently all operate on the same tmp directory.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
 .cargo/config.toml                            |   3 +
 .gitignore                                    |   1 +
 Cargo.toml                                    |   2 +
 tests/migration.rs                            | 185 +++++++
 tests/resources/compare/pve-node-9.0_testnode | 501 ++++++++++++++++++
 .../compare/pve-storage-9.0_testnode_iso      |  93 ++++
 tests/resources/compare/pve-vm-9.0_100        | 453 ++++++++++++++++
 tests/resources/compare/second_empty_run      |   8 +
 .../resources/compare/second_run_with_missed  |   7 +
 tests/resources/resourcelists/.members        |  10 +
 tests/resources/resourcelists/.vmlist         |   7 +
 .../resources/source/pve2-node/othernode.old  | Bin 0 -> 81008 bytes
 tests/resources/source/pve2-node/testnode     | Bin 0 -> 81008 bytes
 .../source/pve2-storage/testnode/foo.old      | Bin 0 -> 14688 bytes
 .../source/pve2-storage/testnode/iso          | Bin 0 -> 14688 bytes
 tests/resources/source/pve2-vm/100            | Bin 0 -> 67744 bytes
 tests/resources/source/pve2-vm/400            | Bin 0 -> 67744 bytes
 tests/resources/source/pve2-vm/500.old        | Bin 0 -> 67744 bytes
 tests/utils.rs                                | 117 ++++
 19 files changed, 1387 insertions(+)
 create mode 100644 tests/migration.rs
 create mode 100644 tests/resources/compare/pve-node-9.0_testnode
 create mode 100644 tests/resources/compare/pve-storage-9.0_testnode_iso
 create mode 100644 tests/resources/compare/pve-vm-9.0_100
 create mode 100644 tests/resources/compare/second_empty_run
 create mode 100644 tests/resources/compare/second_run_with_missed
 create mode 100644 tests/resources/resourcelists/.members
 create mode 100644 tests/resources/resourcelists/.vmlist
 create mode 100644 tests/resources/source/pve2-node/othernode.old
 create mode 100644 tests/resources/source/pve2-node/testnode
 create mode 100644 tests/resources/source/pve2-storage/testnode/foo.old
 create mode 100644 tests/resources/source/pve2-storage/testnode/iso
 create mode 100644 tests/resources/source/pve2-vm/100
 create mode 100644 tests/resources/source/pve2-vm/400
 create mode 100644 tests/resources/source/pve2-vm/500.old
 create mode 100644 tests/utils.rs

diff --git a/.cargo/config.toml b/.cargo/config.toml
index 3b5b6e4..cf8bc1e 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -3,3 +3,6 @@
 directory = "/usr/share/cargo/registry"
 [source.crates-io]
 replace-with = "debian-packages"
+[env]
+# as they currently use the same tmp_tests dir to perform the tests
+RUST_TEST_THREADS = "1"
diff --git a/.gitignore b/.gitignore
index 06ac1a1..d8d3016 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
 target/
 /Cargo.lock
 /proxmox-rrd-migration-tool-[0-9]*/
+/tmp_tests
diff --git a/Cargo.toml b/Cargo.toml
index d3523f3..a24b79c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,3 +18,5 @@ crossbeam-channel = "0.5"
 [build-dependencies]
 bindgen = "0.66.1"
 pkg-config = "0.3"
+[dev-dependencies]
+pretty_assertions = "1.4"
diff --git a/tests/migration.rs b/tests/migration.rs
new file mode 100644
index 0000000..ea425d5
--- /dev/null
+++ b/tests/migration.rs
@@ -0,0 +1,185 @@
+use anyhow::Error;
+use pretty_assertions::assert_eq;
+use std::{
+    fs,
+    path::{Path, PathBuf},
+    process::Command,
+};
+
+mod utils;
+
+use utils::{TMPDIR, TMPDIR_RESOURCELISTS, TMPDIR_SOURCE_BASEDIR, TMPDIR_TARGET};
+
+const TARGET_SUBDIR_NODE: &str = "pve-node-9.0";
+const TARGET_SUBDIR_GUEST: &str = "pve-vm-9.0";
+const TARGET_SUBDIR_STORAGE: &str = "pve-storage-9.0";
+
+#[test]
+fn migration() {
+    utils::test_prepare();
+
+    let target_dir_guests: PathBuf = [TMPDIR_TARGET, TARGET_SUBDIR_GUEST].iter().collect();
+    let target_dir_nodes: PathBuf = [TMPDIR_TARGET, TARGET_SUBDIR_NODE].iter().collect();
+    let target_dir_storage: PathBuf = [TMPDIR_TARGET, TARGET_SUBDIR_STORAGE].iter().collect();
+
+    // first test, compare resulting rrd files
+    Command::new("faketime")
+        .arg("2025-08-01 00:00:00")
+        .arg(utils::migration_tool_path())
+        .arg("--migrate")
+        .arg("--source")
+        .arg(TMPDIR_SOURCE_BASEDIR)
+        .arg("--target")
+        .arg(TMPDIR_TARGET)
+        .arg("--resources")
+        .arg(TMPDIR_RESOURCELISTS)
+        .output()
+        .expect("failed to execute proxmox-rrd-migration-tool");
+
+    // assert target files as we expect them
+    assert!(Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_NODE}/testnode").as_str()).exists());
+    assert!(Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_GUEST}/100").as_str()).exists());
+    assert!(!Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_GUEST}/400").as_str()).exists());
+    assert!(!Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_GUEST}/400.old").as_str()).exists());
+    assert!(!Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_GUEST}/500.old").as_str()).exists());
+    assert!(
+        Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_STORAGE}/testnode/iso").as_str())
+            .exists()
+    );
+    assert!(Path::new(format!("{TMPDIR_SOURCE_BASEDIR}/pve2-vm/100.old").as_str()).exists());
+    assert!(Path::new(format!("{TMPDIR_SOURCE_BASEDIR}/pve2-vm/400.old").as_str()).exists());
+
+    // compare
+    utils::compare_results("node", &target_dir_nodes, &TARGET_SUBDIR_NODE);
+
+    utils::compare_results("guest", &target_dir_guests, &TARGET_SUBDIR_GUEST);
+
+    // storage has another layer of directories per node over which we need to iterate
+    fs::read_dir(&target_dir_storage)
+        .expect("could not read target storage dir")
+        .filter(|f| f.is_ok())
+        .map(|f| f.unwrap().path())
+        .filter(|f| f.is_dir())
+        .try_for_each(|node| {
+            let mut source_storage_subdir = target_dir_storage.clone();
+            source_storage_subdir.push(node.file_name().unwrap());
+
+            let mut target_storage_subdir = target_dir_storage.clone();
+            target_storage_subdir.push(node.file_name().unwrap());
+
+            utils::compare_results(
+                "storage",
+                &source_storage_subdir,
+                format!(
+                    "{TARGET_SUBDIR_STORAGE}_{}",
+                    node.file_name().unwrap().to_string_lossy()
+                )
+                .as_str(),
+            );
+            Ok::<(), Error>(())
+        })
+        .expect("Error running storage test");
+}
+#[test]
+fn migration_second_empty_run() {
+    utils::test_prepare();
+
+    // run initial migration
+    Command::new("faketime")
+        .arg("2025-08-01 00:00:00")
+        .arg(utils::migration_tool_path())
+        .arg("--migrate")
+        .arg("--source")
+        .arg(TMPDIR_SOURCE_BASEDIR)
+        .arg("--target")
+        .arg(TMPDIR_TARGET)
+        .arg("--resources")
+        .arg(TMPDIR_RESOURCELISTS)
+        .output()
+        .expect("failed to execute proxmox-rrd-migration-tool");
+
+    // check if output skips all currently existing files
+    let output = Command::new("faketime")
+        .arg("2025-08-01 00:00:00")
+        .arg(utils::migration_tool_path())
+        .arg("--threads")
+        .arg("2")
+        .arg("--migrate")
+        .arg("--source")
+        .arg(TMPDIR_SOURCE_BASEDIR)
+        .arg("--target")
+        .arg(TMPDIR_TARGET)
+        .arg("--resources")
+        .arg(TMPDIR_RESOURCELISTS)
+        .output()
+        .expect("failed to execute proxmox-rrd-migration-tool");
+    let expected_path: PathBuf = [TMPDIR, "resources", "compare", "second_empty_run"]
+        .iter()
+        .collect();
+
+    let expected =
+        fs::read_to_string(expected_path).expect("could not read compare file for skip all");
+
+    assert_eq!(
+        expected,
+        String::from_utf8(output.stdout).expect("could not parse output")
+    );
+}
+
+#[test]
+fn migration_second_run_with_missed_files() {
+    utils::test_prepare();
+
+    // run initial migration
+    Command::new("faketime")
+        .arg("2025-08-01 00:00:00")
+        .arg(utils::migration_tool_path())
+        .arg("--migrate")
+        .arg("--source")
+        .arg(TMPDIR_SOURCE_BASEDIR)
+        .arg("--target")
+        .arg(TMPDIR_TARGET)
+        .arg("--resources")
+        .arg(TMPDIR_RESOURCELISTS)
+        .output()
+        .expect("failed to execute proxmox-rrd-migration-tool");
+
+    let src_vm = format!("{TMPDIR_SOURCE_BASEDIR}/pve2-vm/100.old");
+    let target_vm = format!("{TMPDIR_SOURCE_BASEDIR}/pve2-vm/101");
+
+    Command::new("cp")
+        .args([src_vm, target_vm])
+        .output()
+        .expect("copy 101 rrd file");
+
+    // check if output skips all currently existing files
+    let output = Command::new("faketime")
+        .arg("2025-08-01 00:00:00")
+        .arg(utils::migration_tool_path())
+        .arg("--threads")
+        .arg("2")
+        .arg("--migrate")
+        .arg("--source")
+        .arg(TMPDIR_SOURCE_BASEDIR)
+        .arg("--target")
+        .arg(TMPDIR_TARGET)
+        .arg("--resources")
+        .arg(TMPDIR_RESOURCELISTS)
+        .output()
+        .expect("failed to execute proxmox-rrd-migration-tool");
+
+    let expected_path: PathBuf = [TMPDIR, "resources", "compare", "second_run_with_missed"]
+        .iter()
+        .collect();
+
+    let expected = fs::read_to_string(expected_path.as_path())
+        .expect("could not read compare file for skip all");
+
+    // drop last line from output as it contains timing information which can change between tests
+    let output = utils::drop_last_line(output.stdout);
+
+    println!("OUTPUT:\n{}", output);
+    println!("EXPECTED:\n{}", expected);
+
+    assert_eq!(expected, output);
+}
diff --git a/tests/resources/compare/pve-node-9.0_testnode b/tests/resources/compare/pve-node-9.0_testnode
new file mode 100644
index 0000000..ebd09a4
--- /dev/null
+++ b/tests/resources/compare/pve-node-9.0_testnode
@@ -0,0 +1,501 @@
+filename = "tmp_tests/target/pve-node-9.0/testnode"
+rrd_version = "0003"
+step = 60
+last_update = 1753999190
+header_size = 17736
+ds[loadavg].index = 0
+ds[loadavg].type = "GAUGE"
+ds[loadavg].minimal_heartbeat = 120
+ds[loadavg].min = 0.0000000000e+00
+ds[loadavg].max = NaN
+ds[loadavg].last_ds = "U"
+ds[loadavg].value = NaN
+ds[loadavg].unknown_sec = 50
+ds[maxcpu].index = 1
+ds[maxcpu].type = "GAUGE"
+ds[maxcpu].minimal_heartbeat = 120
+ds[maxcpu].min = 0.0000000000e+00
+ds[maxcpu].max = NaN
+ds[maxcpu].last_ds = "U"
+ds[maxcpu].value = NaN
+ds[maxcpu].unknown_sec = 50
+ds[cpu].index = 2
+ds[cpu].type = "GAUGE"
+ds[cpu].minimal_heartbeat = 120
+ds[cpu].min = 0.0000000000e+00
+ds[cpu].max = NaN
+ds[cpu].last_ds = "U"
+ds[cpu].value = NaN
+ds[cpu].unknown_sec = 50
+ds[iowait].index = 3
+ds[iowait].type = "GAUGE"
+ds[iowait].minimal_heartbeat = 120
+ds[iowait].min = 0.0000000000e+00
+ds[iowait].max = NaN
+ds[iowait].last_ds = "U"
+ds[iowait].value = NaN
+ds[iowait].unknown_sec = 50
+ds[memtotal].index = 4
+ds[memtotal].type = "GAUGE"
+ds[memtotal].minimal_heartbeat = 120
+ds[memtotal].min = 0.0000000000e+00
+ds[memtotal].max = NaN
+ds[memtotal].last_ds = "U"
+ds[memtotal].value = NaN
+ds[memtotal].unknown_sec = 50
+ds[memused].index = 5
+ds[memused].type = "GAUGE"
+ds[memused].minimal_heartbeat = 120
+ds[memused].min = 0.0000000000e+00
+ds[memused].max = NaN
+ds[memused].last_ds = "U"
+ds[memused].value = NaN
+ds[memused].unknown_sec = 50
+ds[swaptotal].index = 6
+ds[swaptotal].type = "GAUGE"
+ds[swaptotal].minimal_heartbeat = 120
+ds[swaptotal].min = 0.0000000000e+00
+ds[swaptotal].max = NaN
+ds[swaptotal].last_ds = "U"
+ds[swaptotal].value = NaN
+ds[swaptotal].unknown_sec = 50
+ds[swapused].index = 7
+ds[swapused].type = "GAUGE"
+ds[swapused].minimal_heartbeat = 120
+ds[swapused].min = 0.0000000000e+00
+ds[swapused].max = NaN
+ds[swapused].last_ds = "U"
+ds[swapused].value = NaN
+ds[swapused].unknown_sec = 50
+ds[roottotal].index = 8
+ds[roottotal].type = "GAUGE"
+ds[roottotal].minimal_heartbeat = 120
+ds[roottotal].min = 0.0000000000e+00
+ds[roottotal].max = NaN
+ds[roottotal].last_ds = "U"
+ds[roottotal].value = NaN
+ds[roottotal].unknown_sec = 50
+ds[rootused].index = 9
+ds[rootused].type = "GAUGE"
+ds[rootused].minimal_heartbeat = 120
+ds[rootused].min = 0.0000000000e+00
+ds[rootused].max = NaN
+ds[rootused].last_ds = "U"
+ds[rootused].value = NaN
+ds[rootused].unknown_sec = 50
+ds[netin].index = 10
+ds[netin].type = "DERIVE"
+ds[netin].minimal_heartbeat = 120
+ds[netin].min = 0.0000000000e+00
+ds[netin].max = NaN
+ds[netin].last_ds = "U"
+ds[netin].value = NaN
+ds[netin].unknown_sec = 50
+ds[netout].index = 11
+ds[netout].type = "DERIVE"
+ds[netout].minimal_heartbeat = 120
+ds[netout].min = 0.0000000000e+00
+ds[netout].max = NaN
+ds[netout].last_ds = "U"
+ds[netout].value = NaN
+ds[netout].unknown_sec = 50
+ds[memfree].index = 12
+ds[memfree].type = "GAUGE"
+ds[memfree].minimal_heartbeat = 120
+ds[memfree].min = 0.0000000000e+00
+ds[memfree].max = NaN
+ds[memfree].last_ds = "U"
+ds[memfree].value = NaN
+ds[memfree].unknown_sec = 50
+ds[arcsize].index = 13
+ds[arcsize].type = "GAUGE"
+ds[arcsize].minimal_heartbeat = 120
+ds[arcsize].min = 0.0000000000e+00
+ds[arcsize].max = NaN
+ds[arcsize].last_ds = "U"
+ds[arcsize].value = NaN
+ds[arcsize].unknown_sec = 50
+ds[pressurecpusome].index = 14
+ds[pressurecpusome].type = "GAUGE"
+ds[pressurecpusome].minimal_heartbeat = 120
+ds[pressurecpusome].min = 0.0000000000e+00
+ds[pressurecpusome].max = NaN
+ds[pressurecpusome].last_ds = "U"
+ds[pressurecpusome].value = NaN
+ds[pressurecpusome].unknown_sec = 50
+ds[pressureiosome].index = 15
+ds[pressureiosome].type = "GAUGE"
+ds[pressureiosome].minimal_heartbeat = 120
+ds[pressureiosome].min = 0.0000000000e+00
+ds[pressureiosome].max = NaN
+ds[pressureiosome].last_ds = "U"
+ds[pressureiosome].value = NaN
+ds[pressureiosome].unknown_sec = 50
+ds[pressureiofull].index = 16
+ds[pressureiofull].type = "GAUGE"
+ds[pressureiofull].minimal_heartbeat = 120
+ds[pressureiofull].min = 0.0000000000e+00
+ds[pressureiofull].max = NaN
+ds[pressureiofull].last_ds = "U"
+ds[pressureiofull].value = NaN
+ds[pressureiofull].unknown_sec = 50
+ds[pressurememorysome].index = 17
+ds[pressurememorysome].type = "GAUGE"
+ds[pressurememorysome].minimal_heartbeat = 120
+ds[pressurememorysome].min = 0.0000000000e+00
+ds[pressurememorysome].max = NaN
+ds[pressurememorysome].last_ds = "U"
+ds[pressurememorysome].value = NaN
+ds[pressurememorysome].unknown_sec = 50
+ds[pressurememoryfull].index = 18
+ds[pressurememoryfull].type = "GAUGE"
+ds[pressurememoryfull].minimal_heartbeat = 120
+ds[pressurememoryfull].min = 0.0000000000e+00
+ds[pressurememoryfull].max = NaN
+ds[pressurememoryfull].last_ds = "U"
+ds[pressurememoryfull].value = NaN
+ds[pressurememoryfull].unknown_sec = 50
+rra[0].cf = "AVERAGE"
+rra[0].rows = 1440
+rra[0].cur_row = 1184
+rra[0].pdp_per_row = 1
+rra[0].xff = 5.0000000000e-01
+rra[0].cdp_prep[0].value = NaN
+rra[0].cdp_prep[0].unknown_datapoints = 0
+rra[0].cdp_prep[1].value = NaN
+rra[0].cdp_prep[1].unknown_datapoints = 0
+rra[0].cdp_prep[2].value = NaN
+rra[0].cdp_prep[2].unknown_datapoints = 0
+rra[0].cdp_prep[3].value = NaN
+rra[0].cdp_prep[3].unknown_datapoints = 0
+rra[0].cdp_prep[4].value = NaN
+rra[0].cdp_prep[4].unknown_datapoints = 0
+rra[0].cdp_prep[5].value = NaN
+rra[0].cdp_prep[5].unknown_datapoints = 0
+rra[0].cdp_prep[6].value = NaN
+rra[0].cdp_prep[6].unknown_datapoints = 0
+rra[0].cdp_prep[7].value = NaN
+rra[0].cdp_prep[7].unknown_datapoints = 0
+rra[0].cdp_prep[8].value = NaN
+rra[0].cdp_prep[8].unknown_datapoints = 0
+rra[0].cdp_prep[9].value = NaN
+rra[0].cdp_prep[9].unknown_datapoints = 0
+rra[0].cdp_prep[10].value = NaN
+rra[0].cdp_prep[10].unknown_datapoints = 0
+rra[0].cdp_prep[11].value = NaN
+rra[0].cdp_prep[11].unknown_datapoints = 0
+rra[0].cdp_prep[12].value = NaN
+rra[0].cdp_prep[12].unknown_datapoints = 0
+rra[0].cdp_prep[13].value = NaN
+rra[0].cdp_prep[13].unknown_datapoints = 0
+rra[0].cdp_prep[14].value = NaN
+rra[0].cdp_prep[14].unknown_datapoints = 0
+rra[0].cdp_prep[15].value = NaN
+rra[0].cdp_prep[15].unknown_datapoints = 0
+rra[0].cdp_prep[16].value = NaN
+rra[0].cdp_prep[16].unknown_datapoints = 0
+rra[0].cdp_prep[17].value = NaN
+rra[0].cdp_prep[17].unknown_datapoints = 0
+rra[0].cdp_prep[18].value = NaN
+rra[0].cdp_prep[18].unknown_datapoints = 0
+rra[1].cf = "AVERAGE"
+rra[1].rows = 1440
+rra[1].cur_row = 1388
+rra[1].pdp_per_row = 30
+rra[1].xff = 5.0000000000e-01
+rra[1].cdp_prep[0].value = 0.0000000000e+00
+rra[1].cdp_prep[0].unknown_datapoints = 29
+rra[1].cdp_prep[1].value = 0.0000000000e+00
+rra[1].cdp_prep[1].unknown_datapoints = 29
+rra[1].cdp_prep[2].value = 0.0000000000e+00
+rra[1].cdp_prep[2].unknown_datapoints = 29
+rra[1].cdp_prep[3].value = 0.0000000000e+00
+rra[1].cdp_prep[3].unknown_datapoints = 29
+rra[1].cdp_prep[4].value = 0.0000000000e+00
+rra[1].cdp_prep[4].unknown_datapoints = 29
+rra[1].cdp_prep[5].value = 0.0000000000e+00
+rra[1].cdp_prep[5].unknown_datapoints = 29
+rra[1].cdp_prep[6].value = 0.0000000000e+00
+rra[1].cdp_prep[6].unknown_datapoints = 29
+rra[1].cdp_prep[7].value = 0.0000000000e+00
+rra[1].cdp_prep[7].unknown_datapoints = 29
+rra[1].cdp_prep[8].value = 0.0000000000e+00
+rra[1].cdp_prep[8].unknown_datapoints = 29
+rra[1].cdp_prep[9].value = 0.0000000000e+00
+rra[1].cdp_prep[9].unknown_datapoints = 29
+rra[1].cdp_prep[10].value = 0.0000000000e+00
+rra[1].cdp_prep[10].unknown_datapoints = 29
+rra[1].cdp_prep[11].value = 0.0000000000e+00
+rra[1].cdp_prep[11].unknown_datapoints = 29
+rra[1].cdp_prep[12].value = 0.0000000000e+00
+rra[1].cdp_prep[12].unknown_datapoints = 29
+rra[1].cdp_prep[13].value = 0.0000000000e+00
+rra[1].cdp_prep[13].unknown_datapoints = 29
+rra[1].cdp_prep[14].value = 0.0000000000e+00
+rra[1].cdp_prep[14].unknown_datapoints = 29
+rra[1].cdp_prep[15].value = 0.0000000000e+00
+rra[1].cdp_prep[15].unknown_datapoints = 29
+rra[1].cdp_prep[16].value = 0.0000000000e+00
+rra[1].cdp_prep[16].unknown_datapoints = 29
+rra[1].cdp_prep[17].value = 0.0000000000e+00
+rra[1].cdp_prep[17].unknown_datapoints = 29
+rra[1].cdp_prep[18].value = 0.0000000000e+00
+rra[1].cdp_prep[18].unknown_datapoints = 29
+rra[2].cf = "AVERAGE"
+rra[2].rows = 1440
+rra[2].cur_row = 130
+rra[2].pdp_per_row = 360
+rra[2].xff = 5.0000000000e-01
+rra[2].cdp_prep[0].value = 0.0000000000e+00
+rra[2].cdp_prep[0].unknown_datapoints = 239
+rra[2].cdp_prep[1].value = 0.0000000000e+00
+rra[2].cdp_prep[1].unknown_datapoints = 239
+rra[2].cdp_prep[2].value = 0.0000000000e+00
+rra[2].cdp_prep[2].unknown_datapoints = 239
+rra[2].cdp_prep[3].value = 0.0000000000e+00
+rra[2].cdp_prep[3].unknown_datapoints = 239
+rra[2].cdp_prep[4].value = 0.0000000000e+00
+rra[2].cdp_prep[4].unknown_datapoints = 239
+rra[2].cdp_prep[5].value = 0.0000000000e+00
+rra[2].cdp_prep[5].unknown_datapoints = 239
+rra[2].cdp_prep[6].value = 0.0000000000e+00
+rra[2].cdp_prep[6].unknown_datapoints = 239
+rra[2].cdp_prep[7].value = 0.0000000000e+00
+rra[2].cdp_prep[7].unknown_datapoints = 239
+rra[2].cdp_prep[8].value = 0.0000000000e+00
+rra[2].cdp_prep[8].unknown_datapoints = 239
+rra[2].cdp_prep[9].value = 0.0000000000e+00
+rra[2].cdp_prep[9].unknown_datapoints = 239
+rra[2].cdp_prep[10].value = 0.0000000000e+00
+rra[2].cdp_prep[10].unknown_datapoints = 239
+rra[2].cdp_prep[11].value = 0.0000000000e+00
+rra[2].cdp_prep[11].unknown_datapoints = 239
+rra[2].cdp_prep[12].value = 0.0000000000e+00
+rra[2].cdp_prep[12].unknown_datapoints = 239
+rra[2].cdp_prep[13].value = 0.0000000000e+00
+rra[2].cdp_prep[13].unknown_datapoints = 239
+rra[2].cdp_prep[14].value = 0.0000000000e+00
+rra[2].cdp_prep[14].unknown_datapoints = 239
+rra[2].cdp_prep[15].value = 0.0000000000e+00
+rra[2].cdp_prep[15].unknown_datapoints = 239
+rra[2].cdp_prep[16].value = 0.0000000000e+00
+rra[2].cdp_prep[16].unknown_datapoints = 239
+rra[2].cdp_prep[17].value = 0.0000000000e+00
+rra[2].cdp_prep[17].unknown_datapoints = 239
+rra[2].cdp_prep[18].value = 0.0000000000e+00
+rra[2].cdp_prep[18].unknown_datapoints = 239
+rra[3].cf = "AVERAGE"
+rra[3].rows = 570
+rra[3].cur_row = 264
+rra[3].pdp_per_row = 10080
+rra[3].xff = 5.0000000000e-01
+rra[3].cdp_prep[0].value = 0.0000000000e+00
+rra[3].cdp_prep[0].unknown_datapoints = 1319
+rra[3].cdp_prep[1].value = 0.0000000000e+00
+rra[3].cdp_prep[1].unknown_datapoints = 1319
+rra[3].cdp_prep[2].value = 0.0000000000e+00
+rra[3].cdp_prep[2].unknown_datapoints = 1319
+rra[3].cdp_prep[3].value = 0.0000000000e+00
+rra[3].cdp_prep[3].unknown_datapoints = 1319
+rra[3].cdp_prep[4].value = 0.0000000000e+00
+rra[3].cdp_prep[4].unknown_datapoints = 1319
+rra[3].cdp_prep[5].value = 0.0000000000e+00
+rra[3].cdp_prep[5].unknown_datapoints = 1319
+rra[3].cdp_prep[6].value = 0.0000000000e+00
+rra[3].cdp_prep[6].unknown_datapoints = 1319
+rra[3].cdp_prep[7].value = 0.0000000000e+00
+rra[3].cdp_prep[7].unknown_datapoints = 1319
+rra[3].cdp_prep[8].value = 0.0000000000e+00
+rra[3].cdp_prep[8].unknown_datapoints = 1319
+rra[3].cdp_prep[9].value = 0.0000000000e+00
+rra[3].cdp_prep[9].unknown_datapoints = 1319
+rra[3].cdp_prep[10].value = 0.0000000000e+00
+rra[3].cdp_prep[10].unknown_datapoints = 1319
+rra[3].cdp_prep[11].value = 0.0000000000e+00
+rra[3].cdp_prep[11].unknown_datapoints = 1319
+rra[3].cdp_prep[12].value = 0.0000000000e+00
+rra[3].cdp_prep[12].unknown_datapoints = 1319
+rra[3].cdp_prep[13].value = 0.0000000000e+00
+rra[3].cdp_prep[13].unknown_datapoints = 1319
+rra[3].cdp_prep[14].value = 0.0000000000e+00
+rra[3].cdp_prep[14].unknown_datapoints = 1319
+rra[3].cdp_prep[15].value = 0.0000000000e+00
+rra[3].cdp_prep[15].unknown_datapoints = 1319
+rra[3].cdp_prep[16].value = 0.0000000000e+00
+rra[3].cdp_prep[16].unknown_datapoints = 1319
+rra[3].cdp_prep[17].value = 0.0000000000e+00
+rra[3].cdp_prep[17].unknown_datapoints = 1319
+rra[3].cdp_prep[18].value = 0.0000000000e+00
+rra[3].cdp_prep[18].unknown_datapoints = 1319
+rra[4].cf = "MAX"
+rra[4].rows = 1440
+rra[4].cur_row = 747
+rra[4].pdp_per_row = 1
+rra[4].xff = 5.0000000000e-01
+rra[4].cdp_prep[0].value = NaN
+rra[4].cdp_prep[0].unknown_datapoints = 0
+rra[4].cdp_prep[1].value = NaN
+rra[4].cdp_prep[1].unknown_datapoints = 0
+rra[4].cdp_prep[2].value = NaN
+rra[4].cdp_prep[2].unknown_datapoints = 0
+rra[4].cdp_prep[3].value = NaN
+rra[4].cdp_prep[3].unknown_datapoints = 0
+rra[4].cdp_prep[4].value = NaN
+rra[4].cdp_prep[4].unknown_datapoints = 0
+rra[4].cdp_prep[5].value = NaN
+rra[4].cdp_prep[5].unknown_datapoints = 0
+rra[4].cdp_prep[6].value = NaN
+rra[4].cdp_prep[6].unknown_datapoints = 0
+rra[4].cdp_prep[7].value = NaN
+rra[4].cdp_prep[7].unknown_datapoints = 0
+rra[4].cdp_prep[8].value = NaN
+rra[4].cdp_prep[8].unknown_datapoints = 0
+rra[4].cdp_prep[9].value = NaN
+rra[4].cdp_prep[9].unknown_datapoints = 0
+rra[4].cdp_prep[10].value = NaN
+rra[4].cdp_prep[10].unknown_datapoints = 0
+rra[4].cdp_prep[11].value = NaN
+rra[4].cdp_prep[11].unknown_datapoints = 0
+rra[4].cdp_prep[12].value = NaN
+rra[4].cdp_prep[12].unknown_datapoints = 0
+rra[4].cdp_prep[13].value = NaN
+rra[4].cdp_prep[13].unknown_datapoints = 0
+rra[4].cdp_prep[14].value = NaN
+rra[4].cdp_prep[14].unknown_datapoints = 0
+rra[4].cdp_prep[15].value = NaN
+rra[4].cdp_prep[15].unknown_datapoints = 0
+rra[4].cdp_prep[16].value = NaN
+rra[4].cdp_prep[16].unknown_datapoints = 0
+rra[4].cdp_prep[17].value = NaN
+rra[4].cdp_prep[17].unknown_datapoints = 0
+rra[4].cdp_prep[18].value = NaN
+rra[4].cdp_prep[18].unknown_datapoints = 0
+rra[5].cf = "MAX"
+rra[5].rows = 1440
+rra[5].cur_row = 574
+rra[5].pdp_per_row = 30
+rra[5].xff = 5.0000000000e-01
+rra[5].cdp_prep[0].value = -inf
+rra[5].cdp_prep[0].unknown_datapoints = 29
+rra[5].cdp_prep[1].value = -inf
+rra[5].cdp_prep[1].unknown_datapoints = 29
+rra[5].cdp_prep[2].value = -inf
+rra[5].cdp_prep[2].unknown_datapoints = 29
+rra[5].cdp_prep[3].value = -inf
+rra[5].cdp_prep[3].unknown_datapoints = 29
+rra[5].cdp_prep[4].value = -inf
+rra[5].cdp_prep[4].unknown_datapoints = 29
+rra[5].cdp_prep[5].value = -inf
+rra[5].cdp_prep[5].unknown_datapoints = 29
+rra[5].cdp_prep[6].value = -inf
+rra[5].cdp_prep[6].unknown_datapoints = 29
+rra[5].cdp_prep[7].value = -inf
+rra[5].cdp_prep[7].unknown_datapoints = 29
+rra[5].cdp_prep[8].value = -inf
+rra[5].cdp_prep[8].unknown_datapoints = 29
+rra[5].cdp_prep[9].value = -inf
+rra[5].cdp_prep[9].unknown_datapoints = 29
+rra[5].cdp_prep[10].value = -inf
+rra[5].cdp_prep[10].unknown_datapoints = 29
+rra[5].cdp_prep[11].value = -inf
+rra[5].cdp_prep[11].unknown_datapoints = 29
+rra[5].cdp_prep[12].value = -inf
+rra[5].cdp_prep[12].unknown_datapoints = 29
+rra[5].cdp_prep[13].value = -inf
+rra[5].cdp_prep[13].unknown_datapoints = 29
+rra[5].cdp_prep[14].value = -inf
+rra[5].cdp_prep[14].unknown_datapoints = 29
+rra[5].cdp_prep[15].value = -inf
+rra[5].cdp_prep[15].unknown_datapoints = 29
+rra[5].cdp_prep[16].value = -inf
+rra[5].cdp_prep[16].unknown_datapoints = 29
+rra[5].cdp_prep[17].value = -inf
+rra[5].cdp_prep[17].unknown_datapoints = 29
+rra[5].cdp_prep[18].value = -inf
+rra[5].cdp_prep[18].unknown_datapoints = 29
+rra[6].cf = "MAX"
+rra[6].rows = 1440
+rra[6].cur_row = 432
+rra[6].pdp_per_row = 360
+rra[6].xff = 5.0000000000e-01
+rra[6].cdp_prep[0].value = -inf
+rra[6].cdp_prep[0].unknown_datapoints = 239
+rra[6].cdp_prep[1].value = -inf
+rra[6].cdp_prep[1].unknown_datapoints = 239
+rra[6].cdp_prep[2].value = -inf
+rra[6].cdp_prep[2].unknown_datapoints = 239
+rra[6].cdp_prep[3].value = -inf
+rra[6].cdp_prep[3].unknown_datapoints = 239
+rra[6].cdp_prep[4].value = -inf
+rra[6].cdp_prep[4].unknown_datapoints = 239
+rra[6].cdp_prep[5].value = -inf
+rra[6].cdp_prep[5].unknown_datapoints = 239
+rra[6].cdp_prep[6].value = -inf
+rra[6].cdp_prep[6].unknown_datapoints = 239
+rra[6].cdp_prep[7].value = -inf
+rra[6].cdp_prep[7].unknown_datapoints = 239
+rra[6].cdp_prep[8].value = -inf
+rra[6].cdp_prep[8].unknown_datapoints = 239
+rra[6].cdp_prep[9].value = -inf
+rra[6].cdp_prep[9].unknown_datapoints = 239
+rra[6].cdp_prep[10].value = -inf
+rra[6].cdp_prep[10].unknown_datapoints = 239
+rra[6].cdp_prep[11].value = -inf
+rra[6].cdp_prep[11].unknown_datapoints = 239
+rra[6].cdp_prep[12].value = -inf
+rra[6].cdp_prep[12].unknown_datapoints = 239
+rra[6].cdp_prep[13].value = -inf
+rra[6].cdp_prep[13].unknown_datapoints = 239
+rra[6].cdp_prep[14].value = -inf
+rra[6].cdp_prep[14].unknown_datapoints = 239
+rra[6].cdp_prep[15].value = -inf
+rra[6].cdp_prep[15].unknown_datapoints = 239
+rra[6].cdp_prep[16].value = -inf
+rra[6].cdp_prep[16].unknown_datapoints = 239
+rra[6].cdp_prep[17].value = -inf
+rra[6].cdp_prep[17].unknown_datapoints = 239
+rra[6].cdp_prep[18].value = -inf
+rra[6].cdp_prep[18].unknown_datapoints = 239
+rra[7].cf = "MAX"
+rra[7].rows = 570
+rra[7].cur_row = 400
+rra[7].pdp_per_row = 10080
+rra[7].xff = 5.0000000000e-01
+rra[7].cdp_prep[0].value = -inf
+rra[7].cdp_prep[0].unknown_datapoints = 1319
+rra[7].cdp_prep[1].value = -inf
+rra[7].cdp_prep[1].unknown_datapoints = 1319
+rra[7].cdp_prep[2].value = -inf
+rra[7].cdp_prep[2].unknown_datapoints = 1319
+rra[7].cdp_prep[3].value = -inf
+rra[7].cdp_prep[3].unknown_datapoints = 1319
+rra[7].cdp_prep[4].value = -inf
+rra[7].cdp_prep[4].unknown_datapoints = 1319
+rra[7].cdp_prep[5].value = -inf
+rra[7].cdp_prep[5].unknown_datapoints = 1319
+rra[7].cdp_prep[6].value = -inf
+rra[7].cdp_prep[6].unknown_datapoints = 1319
+rra[7].cdp_prep[7].value = -inf
+rra[7].cdp_prep[7].unknown_datapoints = 1319
+rra[7].cdp_prep[8].value = -inf
+rra[7].cdp_prep[8].unknown_datapoints = 1319
+rra[7].cdp_prep[9].value = -inf
+rra[7].cdp_prep[9].unknown_datapoints = 1319
+rra[7].cdp_prep[10].value = -inf
+rra[7].cdp_prep[10].unknown_datapoints = 1319
+rra[7].cdp_prep[11].value = -inf
+rra[7].cdp_prep[11].unknown_datapoints = 1319
+rra[7].cdp_prep[12].value = -inf
+rra[7].cdp_prep[12].unknown_datapoints = 1319
+rra[7].cdp_prep[13].value = -inf
+rra[7].cdp_prep[13].unknown_datapoints = 1319
+rra[7].cdp_prep[14].value = -inf
+rra[7].cdp_prep[14].unknown_datapoints = 1319
+rra[7].cdp_prep[15].value = -inf
+rra[7].cdp_prep[15].unknown_datapoints = 1319
+rra[7].cdp_prep[16].value = -inf
+rra[7].cdp_prep[16].unknown_datapoints = 1319
+rra[7].cdp_prep[17].value = -inf
+rra[7].cdp_prep[17].unknown_datapoints = 1319
+rra[7].cdp_prep[18].value = -inf
+rra[7].cdp_prep[18].unknown_datapoints = 1319
diff --git a/tests/resources/compare/pve-storage-9.0_testnode_iso b/tests/resources/compare/pve-storage-9.0_testnode_iso
new file mode 100644
index 0000000..31d9bd8
--- /dev/null
+++ b/tests/resources/compare/pve-storage-9.0_testnode_iso
@@ -0,0 +1,93 @@
+filename = "tmp_tests/target/pve-storage-9.0/testnode/iso"
+rrd_version = "0003"
+step = 60
+last_update = 1753999190
+header_size = 2912
+ds[total].index = 0
+ds[total].type = "GAUGE"
+ds[total].minimal_heartbeat = 120
+ds[total].min = 0.0000000000e+00
+ds[total].max = NaN
+ds[total].last_ds = "U"
+ds[total].value = NaN
+ds[total].unknown_sec = 50
+ds[used].index = 1
+ds[used].type = "GAUGE"
+ds[used].minimal_heartbeat = 120
+ds[used].min = 0.0000000000e+00
+ds[used].max = NaN
+ds[used].last_ds = "U"
+ds[used].value = NaN
+ds[used].unknown_sec = 50
+rra[0].cf = "AVERAGE"
+rra[0].rows = 1440
+rra[0].cur_row = 304
+rra[0].pdp_per_row = 1
+rra[0].xff = 5.0000000000e-01
+rra[0].cdp_prep[0].value = NaN
+rra[0].cdp_prep[0].unknown_datapoints = 0
+rra[0].cdp_prep[1].value = NaN
+rra[0].cdp_prep[1].unknown_datapoints = 0
+rra[1].cf = "AVERAGE"
+rra[1].rows = 1440
+rra[1].cur_row = 1136
+rra[1].pdp_per_row = 30
+rra[1].xff = 5.0000000000e-01
+rra[1].cdp_prep[0].value = 0.0000000000e+00
+rra[1].cdp_prep[0].unknown_datapoints = 29
+rra[1].cdp_prep[1].value = 0.0000000000e+00
+rra[1].cdp_prep[1].unknown_datapoints = 29
+rra[2].cf = "AVERAGE"
+rra[2].rows = 1440
+rra[2].cur_row = 438
+rra[2].pdp_per_row = 360
+rra[2].xff = 5.0000000000e-01
+rra[2].cdp_prep[0].value = 0.0000000000e+00
+rra[2].cdp_prep[0].unknown_datapoints = 239
+rra[2].cdp_prep[1].value = 0.0000000000e+00
+rra[2].cdp_prep[1].unknown_datapoints = 239
+rra[3].cf = "AVERAGE"
+rra[3].rows = 570
+rra[3].cur_row = 430
+rra[3].pdp_per_row = 10080
+rra[3].xff = 5.0000000000e-01
+rra[3].cdp_prep[0].value = 0.0000000000e+00
+rra[3].cdp_prep[0].unknown_datapoints = 1319
+rra[3].cdp_prep[1].value = 0.0000000000e+00
+rra[3].cdp_prep[1].unknown_datapoints = 1319
+rra[4].cf = "MAX"
+rra[4].rows = 1440
+rra[4].cur_row = 945
+rra[4].pdp_per_row = 1
+rra[4].xff = 5.0000000000e-01
+rra[4].cdp_prep[0].value = NaN
+rra[4].cdp_prep[0].unknown_datapoints = 0
+rra[4].cdp_prep[1].value = NaN
+rra[4].cdp_prep[1].unknown_datapoints = 0
+rra[5].cf = "MAX"
+rra[5].rows = 1440
+rra[5].cur_row = 356
+rra[5].pdp_per_row = 30
+rra[5].xff = 5.0000000000e-01
+rra[5].cdp_prep[0].value = -inf
+rra[5].cdp_prep[0].unknown_datapoints = 29
+rra[5].cdp_prep[1].value = -inf
+rra[5].cdp_prep[1].unknown_datapoints = 29
+rra[6].cf = "MAX"
+rra[6].rows = 1440
+rra[6].cur_row = 1349
+rra[6].pdp_per_row = 360
+rra[6].xff = 5.0000000000e-01
+rra[6].cdp_prep[0].value = -inf
+rra[6].cdp_prep[0].unknown_datapoints = 239
+rra[6].cdp_prep[1].value = -inf
+rra[6].cdp_prep[1].unknown_datapoints = 239
+rra[7].cf = "MAX"
+rra[7].rows = 570
+rra[7].cur_row = 421
+rra[7].pdp_per_row = 10080
+rra[7].xff = 5.0000000000e-01
+rra[7].cdp_prep[0].value = -inf
+rra[7].cdp_prep[0].unknown_datapoints = 1319
+rra[7].cdp_prep[1].value = -inf
+rra[7].cdp_prep[1].unknown_datapoints = 1319
diff --git a/tests/resources/compare/pve-vm-9.0_100 b/tests/resources/compare/pve-vm-9.0_100
new file mode 100644
index 0000000..9658dc4
--- /dev/null
+++ b/tests/resources/compare/pve-vm-9.0_100
@@ -0,0 +1,453 @@
+filename = "tmp_tests/target/pve-vm-9.0/100"
+rrd_version = "0003"
+step = 60
+last_update = 1753999190
+header_size = 15992
+ds[maxcpu].index = 0
+ds[maxcpu].type = "GAUGE"
+ds[maxcpu].minimal_heartbeat = 120
+ds[maxcpu].min = 0.0000000000e+00
+ds[maxcpu].max = NaN
+ds[maxcpu].last_ds = "U"
+ds[maxcpu].value = NaN
+ds[maxcpu].unknown_sec = 50
+ds[cpu].index = 1
+ds[cpu].type = "GAUGE"
+ds[cpu].minimal_heartbeat = 120
+ds[cpu].min = 0.0000000000e+00
+ds[cpu].max = NaN
+ds[cpu].last_ds = "U"
+ds[cpu].value = NaN
+ds[cpu].unknown_sec = 50
+ds[maxmem].index = 2
+ds[maxmem].type = "GAUGE"
+ds[maxmem].minimal_heartbeat = 120
+ds[maxmem].min = 0.0000000000e+00
+ds[maxmem].max = NaN
+ds[maxmem].last_ds = "U"
+ds[maxmem].value = NaN
+ds[maxmem].unknown_sec = 50
+ds[mem].index = 3
+ds[mem].type = "GAUGE"
+ds[mem].minimal_heartbeat = 120
+ds[mem].min = 0.0000000000e+00
+ds[mem].max = NaN
+ds[mem].last_ds = "U"
+ds[mem].value = NaN
+ds[mem].unknown_sec = 50
+ds[maxdisk].index = 4
+ds[maxdisk].type = "GAUGE"
+ds[maxdisk].minimal_heartbeat = 120
+ds[maxdisk].min = 0.0000000000e+00
+ds[maxdisk].max = NaN
+ds[maxdisk].last_ds = "U"
+ds[maxdisk].value = NaN
+ds[maxdisk].unknown_sec = 50
+ds[disk].index = 5
+ds[disk].type = "GAUGE"
+ds[disk].minimal_heartbeat = 120
+ds[disk].min = 0.0000000000e+00
+ds[disk].max = NaN
+ds[disk].last_ds = "U"
+ds[disk].value = NaN
+ds[disk].unknown_sec = 50
+ds[netin].index = 6
+ds[netin].type = "DERIVE"
+ds[netin].minimal_heartbeat = 120
+ds[netin].min = 0.0000000000e+00
+ds[netin].max = NaN
+ds[netin].last_ds = "U"
+ds[netin].value = NaN
+ds[netin].unknown_sec = 50
+ds[netout].index = 7
+ds[netout].type = "DERIVE"
+ds[netout].minimal_heartbeat = 120
+ds[netout].min = 0.0000000000e+00
+ds[netout].max = NaN
+ds[netout].last_ds = "U"
+ds[netout].value = NaN
+ds[netout].unknown_sec = 50
+ds[diskread].index = 8
+ds[diskread].type = "DERIVE"
+ds[diskread].minimal_heartbeat = 120
+ds[diskread].min = 0.0000000000e+00
+ds[diskread].max = NaN
+ds[diskread].last_ds = "U"
+ds[diskread].value = NaN
+ds[diskread].unknown_sec = 50
+ds[diskwrite].index = 9
+ds[diskwrite].type = "DERIVE"
+ds[diskwrite].minimal_heartbeat = 120
+ds[diskwrite].min = 0.0000000000e+00
+ds[diskwrite].max = NaN
+ds[diskwrite].last_ds = "U"
+ds[diskwrite].value = NaN
+ds[diskwrite].unknown_sec = 50
+ds[memhost].index = 10
+ds[memhost].type = "GAUGE"
+ds[memhost].minimal_heartbeat = 120
+ds[memhost].min = 0.0000000000e+00
+ds[memhost].max = NaN
+ds[memhost].last_ds = "U"
+ds[memhost].value = NaN
+ds[memhost].unknown_sec = 50
+ds[pressurecpusome].index = 11
+ds[pressurecpusome].type = "GAUGE"
+ds[pressurecpusome].minimal_heartbeat = 120
+ds[pressurecpusome].min = 0.0000000000e+00
+ds[pressurecpusome].max = NaN
+ds[pressurecpusome].last_ds = "U"
+ds[pressurecpusome].value = NaN
+ds[pressurecpusome].unknown_sec = 50
+ds[pressurecpufull].index = 12
+ds[pressurecpufull].type = "GAUGE"
+ds[pressurecpufull].minimal_heartbeat = 120
+ds[pressurecpufull].min = 0.0000000000e+00
+ds[pressurecpufull].max = NaN
+ds[pressurecpufull].last_ds = "U"
+ds[pressurecpufull].value = NaN
+ds[pressurecpufull].unknown_sec = 50
+ds[pressureiosome].index = 13
+ds[pressureiosome].type = "GAUGE"
+ds[pressureiosome].minimal_heartbeat = 120
+ds[pressureiosome].min = 0.0000000000e+00
+ds[pressureiosome].max = NaN
+ds[pressureiosome].last_ds = "U"
+ds[pressureiosome].value = NaN
+ds[pressureiosome].unknown_sec = 50
+ds[pressureiofull].index = 14
+ds[pressureiofull].type = "GAUGE"
+ds[pressureiofull].minimal_heartbeat = 120
+ds[pressureiofull].min = 0.0000000000e+00
+ds[pressureiofull].max = NaN
+ds[pressureiofull].last_ds = "U"
+ds[pressureiofull].value = NaN
+ds[pressureiofull].unknown_sec = 50
+ds[pressurememorysome].index = 15
+ds[pressurememorysome].type = "GAUGE"
+ds[pressurememorysome].minimal_heartbeat = 120
+ds[pressurememorysome].min = 0.0000000000e+00
+ds[pressurememorysome].max = NaN
+ds[pressurememorysome].last_ds = "U"
+ds[pressurememorysome].value = NaN
+ds[pressurememorysome].unknown_sec = 50
+ds[pressurememoryfull].index = 16
+ds[pressurememoryfull].type = "GAUGE"
+ds[pressurememoryfull].minimal_heartbeat = 120
+ds[pressurememoryfull].min = 0.0000000000e+00
+ds[pressurememoryfull].max = NaN
+ds[pressurememoryfull].last_ds = "U"
+ds[pressurememoryfull].value = NaN
+ds[pressurememoryfull].unknown_sec = 50
+rra[0].cf = "AVERAGE"
+rra[0].rows = 1440
+rra[0].cur_row = 1099
+rra[0].pdp_per_row = 1
+rra[0].xff = 5.0000000000e-01
+rra[0].cdp_prep[0].value = NaN
+rra[0].cdp_prep[0].unknown_datapoints = 0
+rra[0].cdp_prep[1].value = NaN
+rra[0].cdp_prep[1].unknown_datapoints = 0
+rra[0].cdp_prep[2].value = NaN
+rra[0].cdp_prep[2].unknown_datapoints = 0
+rra[0].cdp_prep[3].value = NaN
+rra[0].cdp_prep[3].unknown_datapoints = 0
+rra[0].cdp_prep[4].value = NaN
+rra[0].cdp_prep[4].unknown_datapoints = 0
+rra[0].cdp_prep[5].value = NaN
+rra[0].cdp_prep[5].unknown_datapoints = 0
+rra[0].cdp_prep[6].value = NaN
+rra[0].cdp_prep[6].unknown_datapoints = 0
+rra[0].cdp_prep[7].value = NaN
+rra[0].cdp_prep[7].unknown_datapoints = 0
+rra[0].cdp_prep[8].value = NaN
+rra[0].cdp_prep[8].unknown_datapoints = 0
+rra[0].cdp_prep[9].value = NaN
+rra[0].cdp_prep[9].unknown_datapoints = 0
+rra[0].cdp_prep[10].value = NaN
+rra[0].cdp_prep[10].unknown_datapoints = 0
+rra[0].cdp_prep[11].value = NaN
+rra[0].cdp_prep[11].unknown_datapoints = 0
+rra[0].cdp_prep[12].value = NaN
+rra[0].cdp_prep[12].unknown_datapoints = 0
+rra[0].cdp_prep[13].value = NaN
+rra[0].cdp_prep[13].unknown_datapoints = 0
+rra[0].cdp_prep[14].value = NaN
+rra[0].cdp_prep[14].unknown_datapoints = 0
+rra[0].cdp_prep[15].value = NaN
+rra[0].cdp_prep[15].unknown_datapoints = 0
+rra[0].cdp_prep[16].value = NaN
+rra[0].cdp_prep[16].unknown_datapoints = 0
+rra[1].cf = "AVERAGE"
+rra[1].rows = 1440
+rra[1].cur_row = 551
+rra[1].pdp_per_row = 30
+rra[1].xff = 5.0000000000e-01
+rra[1].cdp_prep[0].value = 0.0000000000e+00
+rra[1].cdp_prep[0].unknown_datapoints = 29
+rra[1].cdp_prep[1].value = 0.0000000000e+00
+rra[1].cdp_prep[1].unknown_datapoints = 29
+rra[1].cdp_prep[2].value = 0.0000000000e+00
+rra[1].cdp_prep[2].unknown_datapoints = 29
+rra[1].cdp_prep[3].value = 0.0000000000e+00
+rra[1].cdp_prep[3].unknown_datapoints = 29
+rra[1].cdp_prep[4].value = 0.0000000000e+00
+rra[1].cdp_prep[4].unknown_datapoints = 29
+rra[1].cdp_prep[5].value = 0.0000000000e+00
+rra[1].cdp_prep[5].unknown_datapoints = 29
+rra[1].cdp_prep[6].value = 0.0000000000e+00
+rra[1].cdp_prep[6].unknown_datapoints = 29
+rra[1].cdp_prep[7].value = 0.0000000000e+00
+rra[1].cdp_prep[7].unknown_datapoints = 29
+rra[1].cdp_prep[8].value = 0.0000000000e+00
+rra[1].cdp_prep[8].unknown_datapoints = 29
+rra[1].cdp_prep[9].value = 0.0000000000e+00
+rra[1].cdp_prep[9].unknown_datapoints = 29
+rra[1].cdp_prep[10].value = 0.0000000000e+00
+rra[1].cdp_prep[10].unknown_datapoints = 29
+rra[1].cdp_prep[11].value = 0.0000000000e+00
+rra[1].cdp_prep[11].unknown_datapoints = 29
+rra[1].cdp_prep[12].value = 0.0000000000e+00
+rra[1].cdp_prep[12].unknown_datapoints = 29
+rra[1].cdp_prep[13].value = 0.0000000000e+00
+rra[1].cdp_prep[13].unknown_datapoints = 29
+rra[1].cdp_prep[14].value = 0.0000000000e+00
+rra[1].cdp_prep[14].unknown_datapoints = 29
+rra[1].cdp_prep[15].value = 0.0000000000e+00
+rra[1].cdp_prep[15].unknown_datapoints = 29
+rra[1].cdp_prep[16].value = 0.0000000000e+00
+rra[1].cdp_prep[16].unknown_datapoints = 29
+rra[2].cf = "AVERAGE"
+rra[2].rows = 1440
+rra[2].cur_row = 1387
+rra[2].pdp_per_row = 360
+rra[2].xff = 5.0000000000e-01
+rra[2].cdp_prep[0].value = 0.0000000000e+00
+rra[2].cdp_prep[0].unknown_datapoints = 239
+rra[2].cdp_prep[1].value = 0.0000000000e+00
+rra[2].cdp_prep[1].unknown_datapoints = 239
+rra[2].cdp_prep[2].value = 0.0000000000e+00
+rra[2].cdp_prep[2].unknown_datapoints = 239
+rra[2].cdp_prep[3].value = 0.0000000000e+00
+rra[2].cdp_prep[3].unknown_datapoints = 239
+rra[2].cdp_prep[4].value = 0.0000000000e+00
+rra[2].cdp_prep[4].unknown_datapoints = 239
+rra[2].cdp_prep[5].value = 0.0000000000e+00
+rra[2].cdp_prep[5].unknown_datapoints = 239
+rra[2].cdp_prep[6].value = 0.0000000000e+00
+rra[2].cdp_prep[6].unknown_datapoints = 239
+rra[2].cdp_prep[7].value = 0.0000000000e+00
+rra[2].cdp_prep[7].unknown_datapoints = 239
+rra[2].cdp_prep[8].value = 0.0000000000e+00
+rra[2].cdp_prep[8].unknown_datapoints = 239
+rra[2].cdp_prep[9].value = 0.0000000000e+00
+rra[2].cdp_prep[9].unknown_datapoints = 239
+rra[2].cdp_prep[10].value = 0.0000000000e+00
+rra[2].cdp_prep[10].unknown_datapoints = 239
+rra[2].cdp_prep[11].value = 0.0000000000e+00
+rra[2].cdp_prep[11].unknown_datapoints = 239
+rra[2].cdp_prep[12].value = 0.0000000000e+00
+rra[2].cdp_prep[12].unknown_datapoints = 239
+rra[2].cdp_prep[13].value = 0.0000000000e+00
+rra[2].cdp_prep[13].unknown_datapoints = 239
+rra[2].cdp_prep[14].value = 0.0000000000e+00
+rra[2].cdp_prep[14].unknown_datapoints = 239
+rra[2].cdp_prep[15].value = 0.0000000000e+00
+rra[2].cdp_prep[15].unknown_datapoints = 239
+rra[2].cdp_prep[16].value = 0.0000000000e+00
+rra[2].cdp_prep[16].unknown_datapoints = 239
+rra[3].cf = "AVERAGE"
+rra[3].rows = 570
+rra[3].cur_row = 100
+rra[3].pdp_per_row = 10080
+rra[3].xff = 5.0000000000e-01
+rra[3].cdp_prep[0].value = 0.0000000000e+00
+rra[3].cdp_prep[0].unknown_datapoints = 1319
+rra[3].cdp_prep[1].value = 0.0000000000e+00
+rra[3].cdp_prep[1].unknown_datapoints = 1319
+rra[3].cdp_prep[2].value = 0.0000000000e+00
+rra[3].cdp_prep[2].unknown_datapoints = 1319
+rra[3].cdp_prep[3].value = 0.0000000000e+00
+rra[3].cdp_prep[3].unknown_datapoints = 1319
+rra[3].cdp_prep[4].value = 0.0000000000e+00
+rra[3].cdp_prep[4].unknown_datapoints = 1319
+rra[3].cdp_prep[5].value = 0.0000000000e+00
+rra[3].cdp_prep[5].unknown_datapoints = 1319
+rra[3].cdp_prep[6].value = 0.0000000000e+00
+rra[3].cdp_prep[6].unknown_datapoints = 1319
+rra[3].cdp_prep[7].value = 0.0000000000e+00
+rra[3].cdp_prep[7].unknown_datapoints = 1319
+rra[3].cdp_prep[8].value = 0.0000000000e+00
+rra[3].cdp_prep[8].unknown_datapoints = 1319
+rra[3].cdp_prep[9].value = 0.0000000000e+00
+rra[3].cdp_prep[9].unknown_datapoints = 1319
+rra[3].cdp_prep[10].value = 0.0000000000e+00
+rra[3].cdp_prep[10].unknown_datapoints = 1319
+rra[3].cdp_prep[11].value = 0.0000000000e+00
+rra[3].cdp_prep[11].unknown_datapoints = 1319
+rra[3].cdp_prep[12].value = 0.0000000000e+00
+rra[3].cdp_prep[12].unknown_datapoints = 1319
+rra[3].cdp_prep[13].value = 0.0000000000e+00
+rra[3].cdp_prep[13].unknown_datapoints = 1319
+rra[3].cdp_prep[14].value = 0.0000000000e+00
+rra[3].cdp_prep[14].unknown_datapoints = 1319
+rra[3].cdp_prep[15].value = 0.0000000000e+00
+rra[3].cdp_prep[15].unknown_datapoints = 1319
+rra[3].cdp_prep[16].value = 0.0000000000e+00
+rra[3].cdp_prep[16].unknown_datapoints = 1319
+rra[4].cf = "MAX"
+rra[4].rows = 1440
+rra[4].cur_row = 216
+rra[4].pdp_per_row = 1
+rra[4].xff = 5.0000000000e-01
+rra[4].cdp_prep[0].value = NaN
+rra[4].cdp_prep[0].unknown_datapoints = 0
+rra[4].cdp_prep[1].value = NaN
+rra[4].cdp_prep[1].unknown_datapoints = 0
+rra[4].cdp_prep[2].value = NaN
+rra[4].cdp_prep[2].unknown_datapoints = 0
+rra[4].cdp_prep[3].value = NaN
+rra[4].cdp_prep[3].unknown_datapoints = 0
+rra[4].cdp_prep[4].value = NaN
+rra[4].cdp_prep[4].unknown_datapoints = 0
+rra[4].cdp_prep[5].value = NaN
+rra[4].cdp_prep[5].unknown_datapoints = 0
+rra[4].cdp_prep[6].value = NaN
+rra[4].cdp_prep[6].unknown_datapoints = 0
+rra[4].cdp_prep[7].value = NaN
+rra[4].cdp_prep[7].unknown_datapoints = 0
+rra[4].cdp_prep[8].value = NaN
+rra[4].cdp_prep[8].unknown_datapoints = 0
+rra[4].cdp_prep[9].value = NaN
+rra[4].cdp_prep[9].unknown_datapoints = 0
+rra[4].cdp_prep[10].value = NaN
+rra[4].cdp_prep[10].unknown_datapoints = 0
+rra[4].cdp_prep[11].value = NaN
+rra[4].cdp_prep[11].unknown_datapoints = 0
+rra[4].cdp_prep[12].value = NaN
+rra[4].cdp_prep[12].unknown_datapoints = 0
+rra[4].cdp_prep[13].value = NaN
+rra[4].cdp_prep[13].unknown_datapoints = 0
+rra[4].cdp_prep[14].value = NaN
+rra[4].cdp_prep[14].unknown_datapoints = 0
+rra[4].cdp_prep[15].value = NaN
+rra[4].cdp_prep[15].unknown_datapoints = 0
+rra[4].cdp_prep[16].value = NaN
+rra[4].cdp_prep[16].unknown_datapoints = 0
+rra[5].cf = "MAX"
+rra[5].rows = 1440
+rra[5].cur_row = 327
+rra[5].pdp_per_row = 30
+rra[5].xff = 5.0000000000e-01
+rra[5].cdp_prep[0].value = -inf
+rra[5].cdp_prep[0].unknown_datapoints = 29
+rra[5].cdp_prep[1].value = -inf
+rra[5].cdp_prep[1].unknown_datapoints = 29
+rra[5].cdp_prep[2].value = -inf
+rra[5].cdp_prep[2].unknown_datapoints = 29
+rra[5].cdp_prep[3].value = -inf
+rra[5].cdp_prep[3].unknown_datapoints = 29
+rra[5].cdp_prep[4].value = -inf
+rra[5].cdp_prep[4].unknown_datapoints = 29
+rra[5].cdp_prep[5].value = -inf
+rra[5].cdp_prep[5].unknown_datapoints = 29
+rra[5].cdp_prep[6].value = -inf
+rra[5].cdp_prep[6].unknown_datapoints = 29
+rra[5].cdp_prep[7].value = -inf
+rra[5].cdp_prep[7].unknown_datapoints = 29
+rra[5].cdp_prep[8].value = -inf
+rra[5].cdp_prep[8].unknown_datapoints = 29
+rra[5].cdp_prep[9].value = -inf
+rra[5].cdp_prep[9].unknown_datapoints = 29
+rra[5].cdp_prep[10].value = -inf
+rra[5].cdp_prep[10].unknown_datapoints = 29
+rra[5].cdp_prep[11].value = -inf
+rra[5].cdp_prep[11].unknown_datapoints = 29
+rra[5].cdp_prep[12].value = -inf
+rra[5].cdp_prep[12].unknown_datapoints = 29
+rra[5].cdp_prep[13].value = -inf
+rra[5].cdp_prep[13].unknown_datapoints = 29
+rra[5].cdp_prep[14].value = -inf
+rra[5].cdp_prep[14].unknown_datapoints = 29
+rra[5].cdp_prep[15].value = -inf
+rra[5].cdp_prep[15].unknown_datapoints = 29
+rra[5].cdp_prep[16].value = -inf
+rra[5].cdp_prep[16].unknown_datapoints = 29
+rra[6].cf = "MAX"
+rra[6].rows = 1440
+rra[6].cur_row = 993
+rra[6].pdp_per_row = 360
+rra[6].xff = 5.0000000000e-01
+rra[6].cdp_prep[0].value = -inf
+rra[6].cdp_prep[0].unknown_datapoints = 239
+rra[6].cdp_prep[1].value = -inf
+rra[6].cdp_prep[1].unknown_datapoints = 239
+rra[6].cdp_prep[2].value = -inf
+rra[6].cdp_prep[2].unknown_datapoints = 239
+rra[6].cdp_prep[3].value = -inf
+rra[6].cdp_prep[3].unknown_datapoints = 239
+rra[6].cdp_prep[4].value = -inf
+rra[6].cdp_prep[4].unknown_datapoints = 239
+rra[6].cdp_prep[5].value = -inf
+rra[6].cdp_prep[5].unknown_datapoints = 239
+rra[6].cdp_prep[6].value = -inf
+rra[6].cdp_prep[6].unknown_datapoints = 239
+rra[6].cdp_prep[7].value = -inf
+rra[6].cdp_prep[7].unknown_datapoints = 239
+rra[6].cdp_prep[8].value = -inf
+rra[6].cdp_prep[8].unknown_datapoints = 239
+rra[6].cdp_prep[9].value = -inf
+rra[6].cdp_prep[9].unknown_datapoints = 239
+rra[6].cdp_prep[10].value = -inf
+rra[6].cdp_prep[10].unknown_datapoints = 239
+rra[6].cdp_prep[11].value = -inf
+rra[6].cdp_prep[11].unknown_datapoints = 239
+rra[6].cdp_prep[12].value = -inf
+rra[6].cdp_prep[12].unknown_datapoints = 239
+rra[6].cdp_prep[13].value = -inf
+rra[6].cdp_prep[13].unknown_datapoints = 239
+rra[6].cdp_prep[14].value = -inf
+rra[6].cdp_prep[14].unknown_datapoints = 239
+rra[6].cdp_prep[15].value = -inf
+rra[6].cdp_prep[15].unknown_datapoints = 239
+rra[6].cdp_prep[16].value = -inf
+rra[6].cdp_prep[16].unknown_datapoints = 239
+rra[7].cf = "MAX"
+rra[7].rows = 570
+rra[7].cur_row = 165
+rra[7].pdp_per_row = 10080
+rra[7].xff = 5.0000000000e-01
+rra[7].cdp_prep[0].value = -inf
+rra[7].cdp_prep[0].unknown_datapoints = 1319
+rra[7].cdp_prep[1].value = -inf
+rra[7].cdp_prep[1].unknown_datapoints = 1319
+rra[7].cdp_prep[2].value = -inf
+rra[7].cdp_prep[2].unknown_datapoints = 1319
+rra[7].cdp_prep[3].value = -inf
+rra[7].cdp_prep[3].unknown_datapoints = 1319
+rra[7].cdp_prep[4].value = -inf
+rra[7].cdp_prep[4].unknown_datapoints = 1319
+rra[7].cdp_prep[5].value = -inf
+rra[7].cdp_prep[5].unknown_datapoints = 1319
+rra[7].cdp_prep[6].value = -inf
+rra[7].cdp_prep[6].unknown_datapoints = 1319
+rra[7].cdp_prep[7].value = -inf
+rra[7].cdp_prep[7].unknown_datapoints = 1319
+rra[7].cdp_prep[8].value = -inf
+rra[7].cdp_prep[8].unknown_datapoints = 1319
+rra[7].cdp_prep[9].value = -inf
+rra[7].cdp_prep[9].unknown_datapoints = 1319
+rra[7].cdp_prep[10].value = -inf
+rra[7].cdp_prep[10].unknown_datapoints = 1319
+rra[7].cdp_prep[11].value = -inf
+rra[7].cdp_prep[11].unknown_datapoints = 1319
+rra[7].cdp_prep[12].value = -inf
+rra[7].cdp_prep[12].unknown_datapoints = 1319
+rra[7].cdp_prep[13].value = -inf
+rra[7].cdp_prep[13].unknown_datapoints = 1319
+rra[7].cdp_prep[14].value = -inf
+rra[7].cdp_prep[14].unknown_datapoints = 1319
+rra[7].cdp_prep[15].value = -inf
+rra[7].cdp_prep[15].unknown_datapoints = 1319
+rra[7].cdp_prep[16].value = -inf
+rra[7].cdp_prep[16].unknown_datapoints = 1319
diff --git a/tests/resources/compare/second_empty_run b/tests/resources/compare/second_empty_run
new file mode 100644
index 0000000..dc1e7f4
--- /dev/null
+++ b/tests/resources/compare/second_empty_run
@@ -0,0 +1,8 @@
+Migrating RRD data for nodes…
+Migrated all nodes
+Migrating RRD data for storages…
+Migrated all storages
+Migrating RRD data for guests…
+Using 2 thread(s)
+Migrated 0 guests
+It took 0.00s
diff --git a/tests/resources/compare/second_run_with_missed b/tests/resources/compare/second_run_with_missed
new file mode 100644
index 0000000..e1c7f71
--- /dev/null
+++ b/tests/resources/compare/second_run_with_missed
@@ -0,0 +1,7 @@
+Migrating RRD data for nodes…
+Migrated all nodes
+Migrating RRD data for storages…
+Migrated all storages
+Migrating RRD data for guests…
+Using 2 thread(s)
+Migrated 1 guests
diff --git a/tests/resources/resourcelists/.members b/tests/resources/resourcelists/.members
new file mode 100644
index 0000000..2823203
--- /dev/null
+++ b/tests/resources/resourcelists/.members
@@ -0,0 +1,10 @@
+{
+"nodename": "testnode",
+"version": 5,
+"cluster": { "name": "rrd-test", "version": 3, "nodes": 3, "quorate": 1 },
+"nodelist": {
+  "testnode": { "id": 1, "online": 1, "ip": "10.9.9.47"},
+  "othernode": { "id": 2, "online": 1, "ip": "10.9.9.48"},
+  "thirdnode": { "id": 3, "online": 1, "ip": "10.9.9.49"}
+  }
+}
diff --git a/tests/resources/resourcelists/.vmlist b/tests/resources/resourcelists/.vmlist
new file mode 100644
index 0000000..d367140
--- /dev/null
+++ b/tests/resources/resourcelists/.vmlist
@@ -0,0 +1,7 @@
+{
+"version": 7,
+"ids": {
+"100": { "node": "testnode", "type": "qemu", "version": 61 },
+"101": { "node": "testnode", "type": "qemu", "version": 61 },
+
+}
diff --git a/tests/resources/source/pve2-node/othernode.old b/tests/resources/source/pve2-node/othernode.old
new file mode 100644
index 0000000000000000000000000000000000000000..9da2327e267563690d2b69a05b976c1870e91836
GIT binary patch
literal 81008
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

literal 0
HcmV?d00001

diff --git a/tests/resources/source/pve2-node/testnode b/tests/resources/source/pve2-node/testnode
new file mode 100644
index 0000000000000000000000000000000000000000..9da2327e267563690d2b69a05b976c1870e91836
GIT binary patch
literal 81008
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

literal 0
HcmV?d00001

diff --git a/tests/resources/source/pve2-storage/testnode/foo.old b/tests/resources/source/pve2-storage/testnode/foo.old
new file mode 100644
index 0000000000000000000000000000000000000000..1f37f59f79b2a5c75eb9f02a24f093bc6c84947b
GIT binary patch
literal 14688
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

literal 0
HcmV?d00001

diff --git a/tests/resources/source/pve2-storage/testnode/iso b/tests/resources/source/pve2-storage/testnode/iso
new file mode 100644
index 0000000000000000000000000000000000000000..1f37f59f79b2a5c75eb9f02a24f093bc6c84947b
GIT binary patch
literal 14688
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

literal 0
HcmV?d00001

diff --git a/tests/resources/source/pve2-vm/100 b/tests/resources/source/pve2-vm/100
new file mode 100644
index 0000000000000000000000000000000000000000..a0c8f286362dfbe63961ed057cf47ed7aa6a78f4
GIT binary patch
literal 67744
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
E1Fg*h5&!@I

literal 0
HcmV?d00001

diff --git a/tests/resources/source/pve2-vm/400 b/tests/resources/source/pve2-vm/400
new file mode 100644
index 0000000000000000000000000000000000000000..a0c8f286362dfbe63961ed057cf47ed7aa6a78f4
GIT binary patch
literal 67744
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
E1Fg*h5&!@I

literal 0
HcmV?d00001

diff --git a/tests/resources/source/pve2-vm/500.old b/tests/resources/source/pve2-vm/500.old
new file mode 100644
index 0000000000000000000000000000000000000000..a0c8f286362dfbe63961ed057cf47ed7aa6a78f4
GIT binary patch
literal 67744
zcmeI5&5ImG9EMwS(I6<~pa;=F6hRLznO)rli8C>4@Sul<1VIpkS#SxPNZcjy?w{bz
zOF)ExSMlP-n^*5%{1f)*ZJnX}UFNOXsqUVx>Fu3<a@cqKqkdKW)Kk@&eD3Vr$kx`@
z23hgr-8cXI>!WvG|KehCto%SXB+mUikH7x*(2L$2-MYE$r?SVvaenyU@u{aPoSf-&
z+@_6w$IrXJyYB;RrQfjA_MZ3d-go!xf!lcAnNC}J-ut@;d-v^Wv+Tz9&L_76p;P-j
z?)Smrfp^?SzZ1?6ckkSt?zydgA3fYV*q!RQjebYBw|7P}yHl2ZT#VzHsqtjlzaQFR
zgM+euU$ffn_50fy_51g;(;c_>ykEVwoPIwYeLh|I<ojiNRo3sayejK=SzeX(yDYED
z`dyY+hcA5p&52CoZ`P*|O25B8EWM@VhFc}qpSSCa|9nnmHa9l6wg&4PTUT`^mRGOG
zu2#9$_VW$a2OC@O4>kwyZB`iGPUGiWtI$izTiWJHf77$qeEsFsn$~$~=yjUbQ~NNd
zpS{-i#kxvWFRY{ET4S2J`Eug%skFwW)cLxp_F>koqx-`;I@a~NsrF&kt)u(HI-d=7
zWwF%yP-fYSixnzbh2y){^u3|>%EaUQ@}%x!kFE8gT&zm0RoeMTJ-;2V_vD9RjBWWf
ztqWzA{Zig2a!0T}o*XpvFH?JE;_>}>QaAEYkxN<^%E+w?UP?M2<1zHA9_yo~b>aGD
zebfUO@ay_|d;06J-t&cb>%F1Ovcn7EJ#~%)eoudDxIfRnKhloxkA~yZdqY`=RkeGQ
zKJU28e$|tY)mYEorFEe?%Go=sol*NRtF=DmuEO6`^};$jKBebMr?f6qM_H+H<R;Zo
zMsAH>Qu{EYjUpGrIy#OtiC$9sFr$qk7sER8JJKY2N$r(K$B91=Hr$`X_ebjS$?qtn
z$VIhRMs8i`QtIdJTRnNb5d3i?wO1Y;C;t4Fe0`)HUsJEuUb*Nz%xjl=KKfQqUeDuO
zc3$n3srhBBkFqI<UQazAeXA$0qmUyP)n0jYocQxw^7WB+d`<U#wO2+^FLWvOeDtT@
zR>Av?BCiyAsmPa$e7nfYMZQ|3{yfb_ah!+Ni9Y9O|JQNofzHoM>g8O;2Y${&T$K3Y
zeDKA2$P*>LI3Ij*9`ZzqFU|*FoQFJ7;*0aa7v~{Ql=$L&@Wpw^6D7VlAAE5h@<fR*
z&Iezdhdfc@i}S%3=OIs&_~Lx<#d*jRCB8Txd~qJ~M2RoX2Vb0rJW=9{^T8MAAy1U}
z;(YMMdB_tbzBnIzaUSwSi7(CvUz~?LQR0j9!58NtPn7uLeDKA2$P*>LI3Ij*9`Zzq
zFV1Jxe9=6UKF{!g$Akd;pL?)s`ycbm{N@Cl!@T_;Uz`*Bf%6Ex!58O)FU|vBoJUst
z{aA6q7w01`O7<7$gD=iQo+$Cf`QVH5kSEHn`=W7@@p*(#KlyZh{W3oHa@F2P>^nZc
ztHy)%>+`#6>$mIkyK3u+=kfIf4|q%noRtS%-~Sk&uMgh8&Ldnm_~Lx<#d+Y1^T_!9
zs2oV~HS{AqW`A*i&U$|d=Z9mTpYzV+m7zb+a}D<^>N(H4>$mGZF~41z-+%7?e&yoN
zmrmAiete<f`t7=ZsmGW4=NqZV*Y$d-`Sq<{*XNh<`F;-%cuWZ30UqE1>wtZLeSm#n
zb@qYq^T*N1{jU4#Ien|w_5HAKzkU_^$L~kw>sepRe1%`y{SW_So^ze&A^th1rte?;
z!@mgu)?4lY`%BJ2KCr(y4{=fAi}S%3=OIs&_~Lx<#d*jRCB8Txd~qJ~M2RoX2Vb0r
zJW=9{^T8MAAy1U};(YMMdB_tbzBnIzaUSwSi7(CvUz~?LQR0j9!58NtPn7uLeDKA2
z$P*>LI3Ij*9`ZzqFU|*FoQFJ7;*0aa7v~{Ql=$L&@Wpw^6D7VlAAE5h@<iEnUo_6X
zWY_0|@%egUe{mk+I>i^~gD=hlUz|tQ@ZT?Cy*W?ToAY43IS<xb&O={AKf+`77x#z#
z#d+Y1^I(6`&f;7`PjuY%{YB#>tIa2z;4~qC2Y7%7>DNKm-<OQf*Zrz}USMDM`Gp5O
zc+aJK&+-27^|Wg559XKo%?UV%dGAqtaZc<9&Li{&Uz`uVIFDuf64vK9&i}meVSn-Y
zWPfoUq0gy(3F~tl=MNwFObFls9^gU$)<O9G<^MOx_W^x9@B3ok{(l3W*WY7xKYyg=
z*Y)+2`g!|SFZKMU=9hYWU9T6u&$EVqf2Qltn|gey`Sq<{>iM9azt7<Tj|l-h=$Z%r
E1Fg*h5&!@I

literal 0
HcmV?d00001

diff --git a/tests/utils.rs b/tests/utils.rs
new file mode 100644
index 0000000..be56095
--- /dev/null
+++ b/tests/utils.rs
@@ -0,0 +1,117 @@
+use pretty_assertions::assert_eq;
+use std::{
+    env, fs,
+    io::{BufRead, Cursor},
+    path::{Path, PathBuf},
+    process::Command,
+};
+
+pub const TMPDIR: &str = "tmp_tests";
+pub const TMPDIR_SOURCE_BASEDIR: &str = "tmp_tests/resources/source";
+pub const TMPDIR_TARGET: &str = "tmp_tests/target";
+pub const TMPDIR_COMPARE: &str = "tmp_tests/resources/compare";
+pub const TMPDIR_RESOURCELISTS: &str = "tmp_tests/resources/resourcelists";
+pub const TEST_RESOURCE_DIR: &str = "tests/resources";
+
+fn get_target_dir() -> PathBuf {
+    let bin = env::current_exe().expect("exe path");
+    let mut target_dir = PathBuf::from(bin.parent().expect("bin parent"));
+    target_dir.pop();
+    target_dir
+}
+
+pub fn migration_tool_path() -> String {
+    let mut target_dir = get_target_dir();
+    target_dir.push("proxmox-rrd-migration-tool");
+    target_dir.to_str().unwrap().to_string()
+}
+
+/// Prepare the directory with the source files on which the tests are performed
+pub fn test_prepare() {
+    let tmpdir = Path::new(TMPDIR);
+
+    println!("Setting up test tmp dir");
+    if tmpdir.exists() {
+        fs::remove_dir_all(tmpdir).expect("remove tmpdir");
+    }
+    fs::create_dir(tmpdir).expect("create tmpdir");
+    fs::create_dir_all(TMPDIR_TARGET).expect("created tmp target dir");
+
+    Command::new("cp")
+        .args(["-ra", TEST_RESOURCE_DIR, TMPDIR])
+        .output()
+        .expect("copy test resource files");
+}
+
+/// Loop over directories to compare results
+///
+/// type:               type of test, node, guest, storage
+/// target_path:        path to the dir where the target RRD files are
+/// comp_subdir_prefix: subdir prefix where the target files are expetect to be per type
+pub fn compare_results(migrationtype: &str, target_path: &PathBuf, comp_subdir_prefix: &str) {
+    fs::read_dir(&target_path)
+        .expect(format!("could not read target {migrationtype} dir").as_str())
+        .filter(|f| f.is_ok())
+        .map(|f| f.unwrap().path())
+        .filter(|f| f.is_file())
+        .for_each(|file| {
+            let path = file.as_path();
+
+            let expected_path: PathBuf = [
+                TMPDIR_COMPARE,
+                format!(
+                    "{}_{}",
+                    comp_subdir_prefix,
+                    file.file_name().unwrap().to_string_lossy()
+                )
+                .as_str(),
+            ]
+            .iter()
+            .collect();
+            let expected = fs::read_to_string(expected_path).expect("read compare file");
+            let testcase = String::from_utf8(
+                Command::new("/usr/bin/rrdtool")
+                    .args(["info", path.to_str().unwrap()])
+                    .output()
+                    .expect("execute rrdtool info")
+                    .stdout,
+            )
+            .expect("rrdtool into to string");
+            compare_rrdinfo_output(testcase, expected);
+        });
+}
+
+/// Compares the output of rrdinfo with the expected output.
+pub fn compare_rrdinfo_output(testcase: String, expected: String) {
+    let expected_lines: Vec<String> = expected.lines().map(|l| String::from(l)).collect();
+    let testcase_lines: Vec<String> = testcase.lines().map(|l| String::from(l)).collect();
+    assert_eq!(
+        expected_lines.len(),
+        testcase_lines.len(),
+        "expected: {}, testcase: {}",
+        expected_lines.len(),
+        testcase_lines.len()
+    );
+    for (expected, command) in expected_lines.iter().zip(testcase_lines.iter()) {
+        if expected.contains("cur_row") || expected.contains("last_update") {
+            // these lines can still have different values regarding timing and ptr positions
+            continue;
+        }
+        assert_eq!(expected, command);
+    }
+}
+
+/// Reads file and resturns it as a string, except for the last line
+pub fn drop_last_line(content: Vec<u8>) -> String {
+    let mut out: Vec<String> = Vec::new();
+    let c = Cursor::new(content);
+    let mut lines = c.lines();
+    while let Some(line) = lines.next() {
+        let line = line.expect("output line");
+        out.push(line);
+    }
+    let _last_line = out.pop();
+    let mut output = out.join("\n");
+    output.push_str("\n");
+    output
+}
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH proxmox-rrd-migration-tool v4 3/3] add debian packaging
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
  2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 1/3] create proxmox-rrd-migration-tool Aaron Lauterer
  2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 2/3] add first tests Aaron Lauterer
@ 2025-07-26  1:05 ` Aaron Lauterer
  2025-07-28 14:36   ` Lukas Wagner
  2025-07-30 17:57   ` [pve-devel] applied: " Thomas Lamprecht
  2025-07-26  1:05 ` [pve-devel] [PATCH cluster v4 1/2] status: introduce new pve-{type}- rrd and metric format Aaron Lauterer
                   ` (30 subsequent siblings)
  33 siblings, 2 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:05 UTC (permalink / raw)
  To: pve-devel

based on the termproxy packaging. Nothing fancy so far.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    I added the links to the repos even though they don't exist yet. So if
    the package and repo name is to change. make sure to adapt those :)

 Cargo.toml           |  4 +-
 Makefile             | 89 ++++++++++++++++++++++++++++++++++++++++++++
 debian/changelog     |  5 +++
 debian/control       | 27 ++++++++++++++
 debian/copyright     | 19 ++++++++++
 debian/docs          |  1 +
 debian/links         |  1 +
 debian/rules         | 30 +++++++++++++++
 debian/source/format |  1 +
 9 files changed, 175 insertions(+), 2 deletions(-)
 create mode 100644 Makefile
 create mode 100644 debian/changelog
 create mode 100644 debian/control
 create mode 100644 debian/copyright
 create mode 100644 debian/docs
 create mode 100644 debian/links
 create mode 100755 debian/rules
 create mode 100644 debian/source/format

diff --git a/Cargo.toml b/Cargo.toml
index a24b79c..e2d49a9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
-name = "proxmox_rrd_migration_8-9"
-version = "0.1.0"
+name = "proxmox-rrd-migration-tool"
+version = "1.0.0"
 edition = "2021"
 authors = [
     "Aaron Lauterer <a.lauterer@proxmox.com>",
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..abce415
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,89 @@
+include /usr/share/dpkg/default.mk
+
+PACKAGE="proxmox-rrd-migration-tool"
+CRATENAME="proxmox-rrd-migration-tool"
+
+BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
+ORIG_SRC_TAR=$(PACKAGE)_$(DEB_VERSION_UPSTREAM).orig.tar.gz
+
+DEB=$(PACKAGE)_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
+DBG_DEB=$(PACKAGE)-dbgsym_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
+DSC=$(PACKAGE)_$(DEB_VERSION).dsc
+
+CARGO ?= cargo
+ifeq ($(BUILD_MODE), release)
+CARGO_BUILD_ARGS += --release
+COMPILEDIR := target/release
+else
+COMPILEDIR := target/debug
+endif
+
+PREFIX = /usr
+LIBEXECDIR = $(PREFIX)/libexec
+PROXMOX_LIBEXECDIR = $(LIBEXECDIR)/proxmox
+
+PROXMOX_RRD_MIGRATION_TOOL_BIN := $(addprefix $(COMPILEDIR)/,proxmox-rrd-migration-tool)
+
+all:
+
+install: $(PROXMOX_RRD_MIGRATION_TOOL_BIN)
+	install -dm755 $(DESTDIR)$(PROXMOX_LIBEXECDIR)
+	install -m755 $(PROXMOX_RRD_MIGRATION_TOOL_BIN) $(DESTDIR)$(PROXMOX_LIBEXECDIR)/
+
+$(PROXMOX_RRD_MIGRATION_TOOL_BIN): .do-cargo-build
+.do-cargo-build:
+	$(CARGO) build $(CARGO_BUILD_ARGS)
+	touch .do-cargo-build
+
+
+.PHONY: cargo-build
+cargo-build: .do-cargo-build
+
+$(BUILDDIR):
+	rm -rf $@ $@.tmp
+	mkdir $@.tmp
+	cp -a debian/ src/ Makefile Cargo.toml wrapper.h build.rs $@.tmp
+	echo "git clone git://git.proxmox.com/git/proxmox-rrd-migration-tool.git\\ngit checkout $$(git rev-parse HEAD)" \
+	    > $@.tmp/debian/SOURCE
+	mv $@.tmp $@
+
+
+$(ORIG_SRC_TAR): $(BUILDDIR)
+	tar czf $(ORIG_SRC_TAR) --exclude="$(BUILDDIR)/debian" $(BUILDDIR)
+
+.PHONY: deb
+deb: $(DEB)
+$(DEB) $(DBG_DEB) &: $(BUILDDIR)
+	cd $(BUILDDIR); dpkg-buildpackage -b -uc -us
+	lintian $(DEB)
+	@echo $(DEB)
+
+.PHONY: dsc
+dsc:
+	rm -rf $(DSC) $(BUILDDIR)
+	$(MAKE) $(DSC)
+	lintian $(DSC)
+
+$(DSC): $(BUILDDIR) $(ORIG_SRC_TAR)
+	cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d
+
+sbuild: $(DSC)
+	sbuild $(DSC)
+
+.PHONY: upload
+upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
+upload: $(DEB) $(DBG_DEB)
+	tar cf - $(DEB) $(DBG_DEB) |ssh -X repoman@repo.proxmox.com -- upload --product pve --dist $(UPLOAD_DIST) --arch $(DEB_HOST_ARCH)
+
+.PHONY: clean distclean
+distclean: clean
+clean:
+	$(CARGO) clean
+	rm -rf $(PACKAGE)-[0-9]*/ build/
+	rm -f *.deb *.changes *.dsc *.tar.* *.buildinfo *.build .do-cargo-build
+	rm -rf tmp_tests
+	rm -rf target
+
+.PHONY: dinstall
+dinstall: deb
+	dpkg -i $(DEB)
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..b82648a
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+proxmox-rrd-migration-tool (1.0.0) unstable; urgency=medium
+
+  * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 21 Jul 2025 13:56:37 +0200
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..8f26878
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,27 @@
+Source: proxmox-rrd-migration-tool
+Section: admin
+Priority: optional
+Build-Depends: cargo:native,
+               debhelper-compat (= 13),
+               dh-cargo (>= 25),
+               librust-anyhow-1+default-dev,
+               librust-bindgen-dev,
+               librust-libc-0.2+default-dev (>= 0.2.107-~~),
+               librust-pico-args-0.5+default-dev,
+               librust-pkg-config-dev,
+               librust-proxmox-async-dev,
+               libstd-rust-dev,
+               rustc:native,
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.6.1
+Vcs-Git: git://git.proxmox.com/git/proxmox-rrd-migration-tool.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox-rrd-migration-tool.git;a=summary
+Homepage: https://www.proxmox.com
+Rules-Requires-Root: no
+
+Package: proxmox-rrd-migration-tool
+Architecture: any
+Multi-Arch: allowed
+Depends: ${misc:Depends}, ${shlibs:Depends},
+Description: Tool to migrate RRD data on Proxmox VE hosts from pre version 8
+  to new version 9 files.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..451848c
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,19 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Source: https://git.proxmox.com/git/proxmox-rrd-migration-tool.git;a=summary
+
+Files:
+ *
+Copyright: 2017 - 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/debian/docs b/debian/docs
new file mode 100644
index 0000000..8696672
--- /dev/null
+++ b/debian/docs
@@ -0,0 +1 @@
+debian/SOURCE
diff --git a/debian/links b/debian/links
new file mode 100644
index 0000000..9e59b57
--- /dev/null
+++ b/debian/links
@@ -0,0 +1 @@
+usr/libexec/proxmox/proxmox-rrd-migration-tool usr/bin/proxmox-rrd-migration-tool
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..ec264eb
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,30 @@
+#!/usr/bin/make -f
+# See debhelper(7) (uncomment to enable)
+# output every command that modifies files on the build system.
+DH_VERBOSE = 1
+
+include /usr/share/dpkg/pkg-info.mk
+include /usr/share/rustc/architecture.mk
+
+export BUILD_MODE=release
+
+CARGO=/usr/share/cargo/bin/cargo
+
+export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
+export DEB_HOST_RUST_TYPE DEB_HOST_GNU_TYPE
+export CARGO_HOME = $(CURDIR)/debian/cargo_home
+
+export DEB_CARGO_CRATE=proxmox-rrd-migration-tool_$(DEB_VERSION_UPSTREAM)
+export DEB_CARGO_PACKAGE=proxmox-rrd-migration-tool
+
+%:
+	dh $@
+
+override_dh_auto_configure:
+	@perl -ne 'if (/^version\s*=\s*"(\d+(?:\.\d+)+)"/) { my $$v_cargo = $$1; my $$v_deb = "$(DEB_VERSION_UPSTREAM)"; \
+	    die "ERROR: d/changelog <-> Cargo.toml version mismatch: $$v_cargo != $$v_deb\n" if $$v_cargo ne $$v_deb; exit(0); }' Cargo.toml
+	$(CARGO) prepare-debian $(CURDIR)/debian/cargo_registry --link-from-system
+	dh_auto_configure
+
+override_dh_missing:
+	dh_missing --fail-missing
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH cluster v4 1/2] status: introduce new pve-{type}- rrd and metric format
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (2 preceding siblings ...)
  2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 3/3] add debian packaging Aaron Lauterer
@ 2025-07-26  1:05 ` Aaron Lauterer
  2025-07-29  9:44   ` Lukas Wagner
  2025-07-26  1:06 ` [pve-devel] [PATCH cluster v4 2/2] rrd: adapt to new RRD format with different aggregation windows Aaron Lauterer
                   ` (29 subsequent siblings)
  33 siblings, 1 reply; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:05 UTC (permalink / raw)
  To: pve-devel

With PVE9 now we have additional fields in the metrics that are
collected and distributed in the cluster. The new fields/columns are
added at the end of the existing ones. This makes it possible for PVE8
installations to still use them by cutting the new additional data.

To make it more future proof, the format of the keys for each metrics
are now changed:

Old pre PVE9:  pve{version}-{type}/{id}
Now with PVE9: pve-{type}-{version}/{id}

This way we have an easier time to handle new versions in the future as
we initially only need to check for `pve-{type}-`. If we know the
version, we can handle it accordingly; e.g. pad if older format with
missing data. If we don't know the version, it must be a newer one and
we cut the data stream at the length we need for the current version.

This means of course that to avoid a breaking change, we can only add
new columns if needed, but not remove any! But waiting for a breaking
change until the next major release is a worthy trade-off if it allows
us to expand the format in between if needed.

The 'rrd_skip_data' function got a new parameter defining the sepataring
character. This then makes it possible to use it also to determine which
part of the key string is the version/type and which one is the actual
resource identifier.

We add several new columns to nodes and VMs (guest) RRDs. See futher
down for details. Additionally we change the RRA definitions on how we
aggregate the data to match how we do it for the Proxmox Backup Server
[0].

The migration of an existing installation is handled by a dedicated
tool. Only once that has happened, will we store data in the new
format.
This leaves us with a few cases to handle:

  data recv →          old                                 new
  ↓ rrd files
 -------------|---------------------------|-------------------------------------
  none        | check if directories exists:
              |     neither old or new -> new
	      |     new                -> new
	      |     old only           -> old
--------------|---------------------------|-------------------------------------
  only old    | use old file as is        | cut new columns and use old file
--------------|---------------------------|-------------------------------------
  new present | pad data to match new fmt | use new file as is and pass data

To handle the padding we use a buffer. Cutting can be handled as we
already do it in the stable/bookworm (PVE8) branch by introducing a null
terminator in the original string at the end of the expected columns.

We add the following new columns:

Nodes:
* memfree
* arcsize
* pressures:
  * cpu some
  * io some
  * io full
  * mem some
  * mem full

VMs:
* memhost (memory consumption of all processes in the guests cgroup, host view)
* pressures:
  * cpu some
  * cpu full
  * io some
  * io full
  * mem some
  * mem full

[0] https://git.proxmox.com/?p=proxmox-backup.git;a=blob;f=src/server/metric_collection/rrd.rs;h=ed39cc94ee056924b7adbc21b84c0209478bcf42;hb=dc324716a688a67d700fa133725740ac5d3795ce#l76

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
 src/pmxcfs/status.c | 261 +++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 236 insertions(+), 25 deletions(-)

diff --git a/src/pmxcfs/status.c b/src/pmxcfs/status.c
index e6b578b..eaef12c 100644
--- a/src/pmxcfs/status.c
+++ b/src/pmxcfs/status.c
@@ -1096,6 +1096,9 @@ kventry_hash_set(GHashTable *kvhash, const char *key, gconstpointer data, size_t
     return TRUE;
 }
 
+// We create the RRD files with a 60 second stepsize, therefore, RRA timesteps
+// are alwys per 60 seconds. These 60 seconds are usually showing up in other
+// code paths where we interact with RRD data!
 static const char *rrd_def_node[] = {
     "DS:loadavg:GAUGE:120:0:U",
     "DS:maxcpu:GAUGE:120:0:U",
@@ -1124,6 +1127,39 @@ static const char *rrd_def_node[] = {
     NULL,
 };
 
+static const char *rrd_def_node_pve9_0[] = {
+    "DS:loadavg:GAUGE:120:0:U",
+    "DS:maxcpu:GAUGE:120:0:U",
+    "DS:cpu:GAUGE:120:0:U",
+    "DS:iowait:GAUGE:120:0:U",
+    "DS:memtotal:GAUGE:120:0:U",
+    "DS:memused:GAUGE:120:0:U",
+    "DS:swaptotal:GAUGE:120:0:U",
+    "DS:swapused:GAUGE:120:0:U",
+    "DS:roottotal:GAUGE:120:0:U",
+    "DS:rootused:GAUGE:120:0:U",
+    "DS:netin:DERIVE:120:0:U",
+    "DS:netout:DERIVE:120:0:U",
+    "DS:memfree:GAUGE:120:0:U",
+    "DS:arcsize:GAUGE:120:0:U",
+    "DS:pressurecpusome:GAUGE:120:0:U",
+    "DS:pressureiosome:GAUGE:120:0:U",
+    "DS:pressureiofull:GAUGE:120:0:U",
+    "DS:pressurememorysome:GAUGE:120:0:U",
+    "DS:pressurememoryfull:GAUGE:120:0:U",
+
+    "RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
+    "RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
+    "RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
+    "RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
+
+    "RRA:MAX:0.5:1:1440",    // 1 min * 1440 => 1 day
+    "RRA:MAX:0.5:30:1440",   // 30 min * 1440 => 30 day
+    "RRA:MAX:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
+    "RRA:MAX:0.5:10080:570", // 1 week * 570 => ~10 years
+    NULL,
+};
+
 static const char *rrd_def_vm[] = {
     "DS:maxcpu:GAUGE:120:0:U",
     "DS:cpu:GAUGE:120:0:U",
@@ -1149,6 +1185,36 @@ static const char *rrd_def_vm[] = {
     "RRA:MAX:0.5:10080:70", // 7 day max - ony year
     NULL,
 };
+static const char *rrd_def_vm_pve9_0[] = {
+    "DS:maxcpu:GAUGE:120:0:U",
+    "DS:cpu:GAUGE:120:0:U",
+    "DS:maxmem:GAUGE:120:0:U",
+    "DS:mem:GAUGE:120:0:U",
+    "DS:maxdisk:GAUGE:120:0:U",
+    "DS:disk:GAUGE:120:0:U",
+    "DS:netin:DERIVE:120:0:U",
+    "DS:netout:DERIVE:120:0:U",
+    "DS:diskread:DERIVE:120:0:U",
+    "DS:diskwrite:DERIVE:120:0:U",
+    "DS:memhost:GAUGE:120:0:U",
+    "DS:pressurecpusome:GAUGE:120:0:U",
+    "DS:pressurecpufull:GAUGE:120:0:U",
+    "DS:pressureiosome:GAUGE:120:0:U",
+    "DS:pressureiofull:GAUGE:120:0:U",
+    "DS:pressurememorysome:GAUGE:120:0:U",
+    "DS:pressurememoryfull:GAUGE:120:0:U",
+
+    "RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
+    "RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
+    "RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
+    "RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
+
+    "RRA:MAX:0.5:1:1440",    // 1 min * 1440 => 1 day
+    "RRA:MAX:0.5:30:1440",   // 30 min * 1440 => 30 day
+    "RRA:MAX:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
+    "RRA:MAX:0.5:10080:570", // 1 week * 570 => ~10 years
+    NULL,
+};
 
 static const char *rrd_def_storage[] = {
     "DS:total:GAUGE:120:0:U",
@@ -1168,8 +1234,30 @@ static const char *rrd_def_storage[] = {
     NULL,
 };
 
+static const char *rrd_def_storage_pve9_0[] = {
+    "DS:total:GAUGE:120:0:U",
+    "DS:used:GAUGE:120:0:U",
+
+    "RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
+    "RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
+    "RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
+    "RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
+
+    "RRA:MAX:0.5:1:1440",    // 1 min * 1440 => 1 day
+    "RRA:MAX:0.5:30:1440",   // 30 min * 1440 => 30 day
+    "RRA:MAX:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
+    "RRA:MAX:0.5:10080:570", // 1 week * 570 => ~10 years
+    NULL,
+};
+
 #define RRDDIR "/var/lib/rrdcached/db"
 
+// A 4k buffer should be plenty to temporarily store RRD data. 64 bit integers are 20 chars long,
+// plus the separator char: (4096-1)/21~195 columns This buffer is only used in the
+// `update_rrd_data` function. It is safe to use as the calling sites get the global mutex:
+// rrd_update_data -> rrdentry_hash_set -> cfs_status_set / and cfs_kvstore_node_set
+static char rrd_format_update_buffer[4096];
+
 static void create_rrd_file(const char *filename, int argcount, const char *rrddef[]) {
     /* start at day boundary */
     time_t ctime;
@@ -1229,6 +1317,8 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
 
     int skip = 0; // columns to skip at beginning. They contain non-archivable data, like uptime,
                   // status, is guest a template and such.
+    int padding = 0; // how many columns need to be added with "U" if we get an old format that is
+                     // missing columns at the end.
     int keep_columns = 0; // how many columns do we want to keep (after initial skip) in case we get
                           // more columns than needed from a newer format
 
@@ -1243,20 +1333,60 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
             goto keyerror;
         }
 
-        skip = 2; // first two columns are live data that isn't archived
+        filename = g_strdup_printf(RRDDIR "/pve-node-9.0/%s", node);
+        char *filename_pve2 = g_strdup_printf(RRDDIR "/pve2-node/%s", node);
 
-        if (strncmp(key, "pve-node-", 9) == 0) {
-            keep_columns = 12; // pve2-node format uses 12 columns
+        int use_pve2_file = 0;
+
+        // check existing rrd files and directories
+        if (g_file_test(filename, G_FILE_TEST_EXISTS)) {
+            // pve-node-9.0 file exists, we use that
+            // TODO: get conditions so, that we do not have this empty branch
+        } else if (g_file_test(filename_pve2, G_FILE_TEST_EXISTS)) {
+            // old file exists, use it
+            use_pve2_file = 1;
+            filename = g_strdup_printf("%s", filename_pve2);
+        } else {
+            // neither file exists, check for directories to decide and create file
+            char *dir_pve2 = g_strdup_printf(RRDDIR "/pve2-node");
+            char *dir_pve90 = g_strdup_printf(RRDDIR "/pve-node-9.0");
+
+            if (g_file_test(dir_pve90, G_FILE_TEST_IS_DIR)) {
+
+                int argcount = sizeof(rrd_def_node_pve9_0) / sizeof(void *) - 1;
+                create_rrd_file(filename, argcount, rrd_def_node_pve9_0);
+            } else if (g_file_test(dir_pve2, G_FILE_TEST_IS_DIR)) {
+                use_pve2_file = 1;
+
+                filename = g_strdup_printf("%s", filename_pve2);
+
+                int argcount = sizeof(rrd_def_node) / sizeof(void *) - 1;
+                create_rrd_file(filename, argcount, rrd_def_node);
+            } else {
+                // no dir exists yet, use new pve-node-9.0
+                mkdir(RRDDIR "/pve-node-9.0", 0755);
+
+                int argcount = sizeof(rrd_def_node_pve9_0) / sizeof(void *) - 1;
+                create_rrd_file(filename, argcount, rrd_def_node_pve9_0);
+            }
+            g_free(dir_pve2);
+            g_free(dir_pve90);
         }
 
-        filename = g_strdup_printf(RRDDIR "/pve2-node/%s", node);
+        skip = 2; // first two columns are live data that isn't archived
 
-        if (!g_file_test(filename, G_FILE_TEST_EXISTS)) {
-            mkdir(RRDDIR "/pve2-node", 0755);
-            int argcount = sizeof(rrd_def_node) / sizeof(void *) - 1;
-            create_rrd_file(filename, argcount, rrd_def_node);
+        if (strncmp(key, "pve2-node/", 10) == 0 && !use_pve2_file) {
+            padding = 7; // pve-node-9.0 has 7 more columns than pve2-node
+        } else if (strncmp(key, "pve-node-", 9) == 0 && use_pve2_file) {
+            keep_columns = 12; // pve2-node format uses 12 columns
+        } else if (strncmp(key, "pve-node-9.0/", 13) != 0) {
+            // we received an unknown format, expectation is it is newer and has more columns
+            // than we can currently handle
+            keep_columns = 19; // pve-node-9.0 format uses 19 columns
         }
 
+        g_free(filename_pve2);
+
     } else if (strncmp(key, "pve2.3-vm/", 10) == 0 || strncmp(key, "pve-vm-", 7) == 0) {
 
         const char *vmid = rrd_skip_data(key, 1, '/');
@@ -1269,20 +1399,60 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
             goto keyerror;
         }
 
-        skip = 4; // first 4 columns are live data that isn't archived
+        filename = g_strdup_printf(RRDDIR "/pve-vm-9.0/%s", vmid);
+        char *filename_pve2 = g_strdup_printf(RRDDIR "/%s/%s", "pve2-vm", vmid);
+
+        int use_pve2_file = 0;
+
+        // check existing rrd files and directories
+        if (g_file_test(filename, G_FILE_TEST_EXISTS)) {
+            // pve-vm-9.0 file exists, we use that
+            // TODO: get conditions so, that we do not have this empty branch
+        } else if (g_file_test(filename_pve2, G_FILE_TEST_EXISTS)) {
+            // old file exists, use it
+            use_pve2_file = 1;
+            filename = g_strdup_printf("%s", filename_pve2);
+        } else {
+            // neither file exists, check for directories to decide and create file
+            char *dir_pve2 = g_strdup_printf(RRDDIR "/pve2-vm");
+            char *dir_pve90 = g_strdup_printf(RRDDIR "/pve-vm-9.0");
+
+            if (g_file_test(dir_pve90, G_FILE_TEST_IS_DIR)) {
 
-        if (strncmp(key, "pve-vm-", 7) == 0) {
-            keep_columns = 10; // pve2.3-vm format uses 10 data columns
+                int argcount = sizeof(rrd_def_vm_pve9_0) / sizeof(void *) - 1;
+                create_rrd_file(filename, argcount, rrd_def_vm_pve9_0);
+            } else if (g_file_test(dir_pve2, G_FILE_TEST_IS_DIR)) {
+                use_pve2_file = 1;
+
+                filename = g_strdup_printf("%s", filename_pve2);
+
+                int argcount = sizeof(rrd_def_vm) / sizeof(void *) - 1;
+                create_rrd_file(filename, argcount, rrd_def_vm);
+            } else {
+                // no dir exists yet, use new pve-vm-9.0
+                mkdir(RRDDIR "/pve-vm-9.0", 0755);
+
+                int argcount = sizeof(rrd_def_vm_pve9_0) / sizeof(void *) - 1;
+                create_rrd_file(filename, argcount, rrd_def_vm_pve9_0);
+            }
+            g_free(dir_pve2);
+            g_free(dir_pve90);
         }
 
-        filename = g_strdup_printf(RRDDIR "/%s/%s", "pve2-vm", vmid);
+        skip = 4; // first 4 columns are live data that isn't archived
 
-        if (!g_file_test(filename, G_FILE_TEST_EXISTS)) {
-            mkdir(RRDDIR "/pve2-vm", 0755);
-            int argcount = sizeof(rrd_def_vm) / sizeof(void *) - 1;
-            create_rrd_file(filename, argcount, rrd_def_vm);
+        if (strncmp(key, "pve2.3-vm/", 10) == 0 && !use_pve2_file) {
+            padding = 7; // pve-vm-9.0 has 7 more columns than pve2.3-vm
+        } else if (strncmp(key, "pve-vm-", 7) == 0 && use_pve2_file) {
+            keep_columns = 10; // pve2.3-vm format uses 10 columns
+        } else if (strncmp(key, "pve-vm-9.0/", 11) != 0) {
+            // we received an unknown format, expectation is it is newer and has more columns
+            // than we can currently handle
+            keep_columns = 17; // pve-vm-9.0 format uses 19 columns
         }
 
+        g_free(filename_pve2);
+
     } else if (strncmp(key, "pve2-storage/", 13) == 0 || strncmp(key, "pve-storage-", 12) == 0) {
         const char *node = rrd_skip_data(key, 1, '/'); // will contain {node}/{storage}
 
@@ -1300,18 +1470,50 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
             goto keyerror;
         }
 
-        filename = g_strdup_printf(RRDDIR "/pve2-storage/%s", node);
+        filename = g_strdup_printf(RRDDIR "/pve-storage-9.0/%s", node);
+        char *filename_pve2 = g_strdup_printf(RRDDIR "/%s/%s", "pve2-storage", node);
+
+        // check existing rrd files and directories
+        if (g_file_test(filename, G_FILE_TEST_EXISTS)) {
+            // pve-storage-9.0 file exists, we use that
+            // TODO: get conditions so, that we do not have this empty branch
+        } else if (g_file_test(filename_pve2, G_FILE_TEST_EXISTS)) {
+            // old file exists, use it
+            filename = g_strdup_printf("%s", filename_pve2);
+        } else {
+            // neither file exists, check for directories to decide and create file
+            char *dir_pve2 = g_strdup_printf(RRDDIR "/pve2-storage");
+            char *dir_pve90 = g_strdup_printf(RRDDIR "/pve-storage-9.0");
+
+            if (g_file_test(dir_pve90, G_FILE_TEST_IS_DIR)) {
+
+                int argcount = sizeof(rrd_def_storage_pve9_0) / sizeof(void *) - 1;
+                create_rrd_file(filename, argcount, rrd_def_storage_pve9_0);
+            } else if (g_file_test(dir_pve2, G_FILE_TEST_IS_DIR)) {
+                filename = g_strdup_printf("%s", filename_pve2);
 
-        if (!g_file_test(filename, G_FILE_TEST_EXISTS)) {
-            mkdir(RRDDIR "/pve2-storage", 0755);
-            char *dir = g_path_get_dirname(filename);
-            mkdir(dir, 0755);
-            g_free(dir);
+                int argcount = sizeof(rrd_def_storage) / sizeof(void *) - 1;
+                create_rrd_file(filename, argcount, rrd_def_storage);
+            } else {
+                // no dir exists yet, use new pve-storage-9.0
+                mkdir(RRDDIR "/pve-storage-9.0", 0755);
 
-            int argcount = sizeof(rrd_def_storage) / sizeof(void *) - 1;
-            create_rrd_file(filename, argcount, rrd_def_storage);
+                int argcount = sizeof(rrd_def_storage_pve9_0) / sizeof(void *) - 1;
+                create_rrd_file(filename, argcount, rrd_def_storage_pve9_0);
+            }
+            g_free(dir_pve2);
+            g_free(dir_pve90);
         }
 
+        // actual data columns didn't change between pve2-storage and pve-storage-9.0
+        if (strncmp(key, "pve-storage-", 12) == 0 && strncmp(key, "pve-storage-9.0/", 16) != 0) {
+            // we received an unknown format, expectation is it is newer and has more columns
+            // than we can currently handle
+            keep_columns = 2; // pve-storage-9.0 format uses 2 columns
+        }
+
+        g_free(filename_pve2);
+
     } else {
         goto keyerror;
     }
@@ -1325,7 +1527,16 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
         *(cut - 1) = 0; // terminate string by replacing colon from field separator with zero.
     }
 
-    const char *update_args[] = {dp, NULL};
+    const char *update_args[] = {NULL, NULL};
+    if (padding) {
+        // add padding "U" columns to data string
+        char *padsrc =
+            ":U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U"; // can pad up to 25 columns
+        g_snprintf(rrd_format_update_buffer, 1024 * 4, "%s%.*s", dp, padding * 2, padsrc);
+        update_args[0] = rrd_format_update_buffer;
+    } else {
+        update_args[0] = dp;
+    }
 
     if (use_daemon) {
         int status;
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH cluster v4 2/2] rrd: adapt to new RRD format with different aggregation windows
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (3 preceding siblings ...)
  2025-07-26  1:05 ` [pve-devel] [PATCH cluster v4 1/2] status: introduce new pve-{type}- rrd and metric format Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 1/4] rrdchart: allow to override the series object Aaron Lauterer
                   ` (28 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

With PVE9 we introduced a new RRD format that has different aggregation
steps, similar to what we use in the Backup Server.
We therefore need to adapt the functions that get data from RRD
accordingly.

The result is usually a finer resolution for time windows larger than
hourly.
We also introduce decade as a time window. In case existing RRD files
have not yet been converted to the new RRD format, we need keep using
the old time windows. Additionally, since they only store data up to a
year, we catch the situation where a full decade might be requested and
pin it to a year.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
 src/PVE/RRD.pm | 52 ++++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 42 insertions(+), 10 deletions(-)

diff --git a/src/PVE/RRD.pm b/src/PVE/RRD.pm
index 93df608..c95f495 100644
--- a/src/PVE/RRD.pm
+++ b/src/PVE/RRD.pm
@@ -14,14 +14,30 @@ sub create_rrd_data {
 
     my $rrd = "$rrddir/$rrdname";
 
+    # Format: [ resolution, number of data points/count]
+    # Old ranges, pre PVE9
+    my $setup_pve2 = {
+        hour => [60, 60], # 1 min resolution, one hour
+        day => [60 * 30, 70], # 30 min resolution, one day
+        week => [60 * 180, 70], # 3 hour resolution, one week
+        month => [60 * 720, 70], # 12 hour resolution, 1 month
+        year => [60 * 10080, 70], # 7 day resolution, 1 year
+    };
+
     my $setup = {
-        hour => [60, 70],
-        day => [60 * 30, 70],
-        week => [60 * 180, 70],
-        month => [60 * 720, 70],
-        year => [60 * 10080, 70],
+        hour => [60, 60], # 1 min resolution
+        day => [60, 1440], # 1 min resolution, full day
+        week => [60 * 30, 336], # 30 min resolution, 7 days
+        month => [3600 * 6, 121], # 6 hour resolution, 30 days, need one more count. Otherwise RRD gets wrong $step
+        year => [3600 * 6, 1140], # 6 hour resolution, 360 days
+        decade => [86400 * 7, 570], # 1 week resolution, 10 years
     };
 
+    if ($rrdname =~ /^pve2/) {
+        $setup = $setup_pve2;
+        $timeframe = "year" if $timeframe eq "decade"; # we only store up to one year in the old format
+    }
+
     my ($reso, $count) = @{ $setup->{$timeframe} };
     my $ctime = $reso * int(time() / $reso);
     my $req_start = $ctime - $reso * $count;
@@ -82,14 +98,30 @@ sub create_rrd_graph {
 
     my $filename = "${rrd}_${ds_txt}.png";
 
+    # Format: [ resolution, number of data points/count]
+    # Old ranges, pre PVE9
+    my $setup_pve2 = {
+        hour => [60, 60], # 1 min resolution, one hour
+        day => [60 * 30, 70], # 30 min resolution, one day
+        week => [60 * 180, 70], # 3 hour resolution, one week
+        month => [60 * 720, 70], # 12 hour resolution, 1 month
+        year => [60 * 10080, 70], # 7 day resolution, 1 year
+    };
+
     my $setup = {
-        hour => [60, 60],
-        day => [60 * 30, 70],
-        week => [60 * 180, 70],
-        month => [60 * 720, 70],
-        year => [60 * 10080, 70],
+        hour => [60, 60], # 1 min resolution
+        day => [60, 1440], # 1 min resolution, full day
+        week => [60 * 30, 336], # 30 min resolution, 7 days
+        month => [3600 * 6, 121], # 6 hour resolution, 30 days, need one more count. Otherwise RRD gets wrong $step
+        year => [3600 * 6, 1140], # 6 hour resolution, 360 days
+        decade => [86400 * 7, 570], # 1 week resolution, 10 years
     };
 
+    if ($rrdname =~ /^pve2/) {
+        $setup = $setup_pve2;
+        $timeframe = "year" if $timeframe eq "decade"; # we only store up to one year in the old format
+    }
+
     my ($reso, $count) = @{ $setup->{$timeframe} };
 
     my @args = (
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH widget-toolkit v4 1/4] rrdchart: allow to override the series object
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (4 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH cluster v4 2/2] rrd: adapt to new RRD format with different aggregation windows Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 2/4] rrdchart: use reference for undo button Aaron Lauterer
                   ` (27 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

this way we can keep the current behavior, but also make it possible to
finely control a series if needed. For example, if we want a stacked
graph, or just a line without fill.

Additionally we need to adjust the tooltip renderer to also gather the
titles from these directly configured series.
We also add a check if the value is null and set the tooltip to "No
Data". Because stacked graphs (which we want to use now), will graph
those data points as well.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
---

Notes:
    changes since:
    v3:
    * reuse 'value' in the tooltip renderer
    * use gettext for "No Data" tooltip

 src/panel/RRDChart.js | 53 +++++++++++++++++++++++++++++++++----------
 1 file changed, 41 insertions(+), 12 deletions(-)

diff --git a/src/panel/RRDChart.js b/src/panel/RRDChart.js
index 86cf4e2..8df6a69 100644
--- a/src/panel/RRDChart.js
+++ b/src/panel/RRDChart.js
@@ -118,13 +118,33 @@ Ext.define('Proxmox.widget.RRDChart', {
                 suffix = 'B/s';
             }
 
-            let prefix = item.field;
-            if (view.fieldTitles && view.fieldTitles[view.fields.indexOf(item.field)]) {
-                prefix = view.fieldTitles[view.fields.indexOf(item.field)];
+            let value = record.get(item.field);
+            if (value === null) {
+                tooltip.setHtml(gettext('No Data'));
+            } else {
+                let prefix = item.field;
+                if (view.fieldTitles && view.fieldTitles[view.fields.indexOf(item.field)]) {
+                    prefix = view.fieldTitles[view.fields.indexOf(item.field)];
+                } else {
+                    // If series is passed in directly, we don't have fieldTitles set. The title property can be a
+                    // single string for a line series, or an array for an area/stacked series.
+                    for (const field of view.fields) {
+                        if (Array.isArray(field.yField)) {
+                            if (field.title && field.title[field.yField.indexOf(item.field)]) {
+                                prefix = field.title[field.yField.indexOf(item.field)];
+                                break;
+                            }
+                        } else if (field.title) {
+                            prefix = field.title;
+                            break;
+                        }
+                    }
+                }
+
+                let v = this.convertToUnits(value);
+                let t = new Date(record.get('time'));
+                tooltip.setHtml(`${prefix}: ${v}${suffix}<br>${t}`);
             }
-            let v = this.convertToUnits(record.get(item.field));
-            let t = new Date(record.get('time'));
-            tooltip.setHtml(`${prefix}: ${v}${suffix}<br>${t}`);
         },
 
         onAfterAnimation: function (chart, eopts) {
@@ -261,17 +281,26 @@ Ext.define('Proxmox.widget.RRDChart', {
 
         // add a series for each field we get
         me.fields.forEach(function (item, index) {
-            let title = item;
-            if (me.fieldTitles && me.fieldTitles[index]) {
-                title = me.fieldTitles[index];
+            let yField;
+            let title;
+            let object;
+
+            if (typeof item === 'object') {
+                object = item;
+            } else {
+                yField = item;
+                title = item;
+                if (me.fieldTitles && me.fieldTitles[index]) {
+                    title = me.fieldTitles[index];
+                }
             }
             me.addSeries(
                 Ext.apply(
                     {
                         type: 'line',
                         xField: 'time',
-                        yField: item,
-                        title: title,
+                        yField,
+                        title,
                         fill: true,
                         style: {
                             lineWidth: 1.5,
@@ -290,7 +319,7 @@ Ext.define('Proxmox.widget.RRDChart', {
                             renderer: 'onSeriesTooltipRender',
                         },
                     },
-                    me.seriesConfig,
+                    object ?? me.seriesConfig,
                 ),
             );
         });
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH widget-toolkit v4 2/4] rrdchart: use reference for undo button
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (5 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 1/4] rrdchart: allow to override the series object Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 3/4] rrdchard: set cursor pointer for legend Aaron Lauterer
                   ` (26 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

This makes targeting the undo button more stable in situations where it
might not be the 0 indexed item in the tools.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/panel/RRDChart.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/panel/RRDChart.js b/src/panel/RRDChart.js
index 8df6a69..35bc186 100644
--- a/src/panel/RRDChart.js
+++ b/src/panel/RRDChart.js
@@ -152,7 +152,7 @@ Ext.define('Proxmox.widget.RRDChart', {
                 return;
             }
             // if the undo button is disabled, disable our tool
-            let ourUndoZoomButton = chart.header.tools[0];
+            let ourUndoZoomButton = chart.lookupReference('undoButton');
             let undoButton = chart.interactions[0].getUndoButton();
             ourUndoZoomButton.setDisabled(undoButton.isDisabled());
         },
@@ -269,6 +269,7 @@ Ext.define('Proxmox.widget.RRDChart', {
             me.addTool({
                 type: 'minus',
                 disabled: true,
+                reference: 'undoButton',
                 tooltip: gettext('Undo Zoom'),
                 handler: function () {
                     let undoButton = me.interactions[0].getUndoButton();
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH widget-toolkit v4 3/4] rrdchard: set cursor pointer for legend
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (6 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 2/4] rrdchart: use reference for undo button Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 4/4] rrdchart: add dummy listener for legend clicks Aaron Lauterer
                   ` (25 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

to make it more obious that the legend items can be clicked

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
 src/panel/RRDChart.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/panel/RRDChart.js b/src/panel/RRDChart.js
index 35bc186..7f0b923 100644
--- a/src/panel/RRDChart.js
+++ b/src/panel/RRDChart.js
@@ -169,6 +169,7 @@ Ext.define('Proxmox.widget.RRDChart', {
     legend: {
         type: 'dom',
         padding: 0,
+        style: 'cursor: pointer;',
     },
     listeners: {
         redraw: {
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH widget-toolkit v4 4/4] rrdchart: add dummy listener for legend clicks
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (7 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 3/4] rrdchard: set cursor pointer for legend Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 01/15] pvestatd: collect and distribute new pve-{type}-9.0 metrics Aaron Lauterer
                   ` (24 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

This way we can define a listener when needed to react to any clicks in
the legend. Usually enabling/disabling some data series.

The event was nowhere documented, but by using the following snippet,
right where we add the listener, it can be observed to happen.

```
Ext.mixin.Observable.capture(this, function(evname) {console.log(evname, arguments);})
```
This would be `me.legend` in that scenario.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
 src/panel/RRDChart.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/panel/RRDChart.js b/src/panel/RRDChart.js
index 7f0b923..c81e2e7 100644
--- a/src/panel/RRDChart.js
+++ b/src/panel/RRDChart.js
@@ -67,6 +67,8 @@ Ext.define('Proxmox.widget.RRDChart', {
     // set to empty string to suppress warning in debug mode
     downloadServerUrl: '-',
 
+    onLegendChange: Ext.emptyFn, // empty dummy function so we can add listener for legend events when needed
+
     controller: {
         xclass: 'Ext.app.ViewController',
 
@@ -261,6 +263,8 @@ Ext.define('Proxmox.widget.RRDChart', {
         me.updateHeader();
 
         if (me.header && me.legend) {
+            // event itemclick is not documented for legend, but found it by printing all events happening
+            me.legend.addListener('itemclick', me.onLegendChange);
             me.header.padding = '4 9 4';
             me.header.add(me.legend);
             me.legend = undefined;
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 01/15] pvestatd: collect and distribute new pve-{type}-9.0 metrics
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (8 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 4/4] rrdchart: add dummy listener for legend clicks Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 02/15] api: nodes: rrd and rrddata add decade option and use new pve-node-9.0 rrd files Aaron Lauterer
                   ` (23 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

If we see that the migration to the new pve-{type}-9.0 rrd format has been done
or is ongoing (new dir exists), we collect and send out the new format with additional
columns for nodes and VMs (guests).

Those are:
Nodes:
* memfree
* arcsize
* pressures:
  * cpu some
  * io some
  * io full
  * mem some
  * mem full

VMs:
* memhost (memory consumption of all processes in the guests cgroup -> host view)
* pressures:
  * cpu some
  * cpu full
  * io some
  * io full
  * mem some
  * mem full

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    changes since:
    RFC:
    * switch from pve9- to pve-{type}-9.0 schema

 PVE/Service/pvestatd.pm | 342 +++++++++++++++++++++++++++++-----------
 1 file changed, 250 insertions(+), 92 deletions(-)

diff --git a/PVE/Service/pvestatd.pm b/PVE/Service/pvestatd.pm
index e645eec3..cfcdaf3d 100755
--- a/PVE/Service/pvestatd.pm
+++ b/PVE/Service/pvestatd.pm
@@ -82,6 +82,16 @@ my $cached_kvm_version = '';
 my $next_flag_update_time;
 my $failed_flag_update_delay_sec = 120;
 
+# Checks if RRD files exist in the specified location.
+my $rrd_dir_exists = sub {
+    my ($location) = @_;
+    if (-d "/var/lib/rrdcached/db/${location}") {
+        return 1;
+    } else {
+        return 0;
+    }
+};
+
 sub update_supported_cpuflags {
     my $kvm_version = PVE::QemuServer::kvm_user_version();
 
@@ -180,32 +190,66 @@ sub update_node_status {
 
     my $meminfo = PVE::ProcFSTools::read_meminfo();
 
+    my $pressures = PVE::ProcFSTools::read_pressure();
+
     my $dinfo = df('/', 1); # output is bytes
     # everything not free is considered to be used
     my $dused = $dinfo->{blocks} - $dinfo->{bfree};
 
     my $ctime = time();
 
-    my $data = $generate_rrd_string->(
-        [
-            $uptime,
-            $sublevel,
-            $ctime,
-            $avg1,
-            $maxcpu,
-            $stat->{cpu},
-            $stat->{wait},
-            $meminfo->{memtotal},
-            $meminfo->{memused},
-            $meminfo->{swaptotal},
-            $meminfo->{swapused},
-            $dinfo->{blocks},
-            $dused,
-            $netin,
-            $netout,
-        ],
-    );
-    PVE::Cluster::broadcast_rrd("pve2-node/$nodename", $data);
+    my $data;
+    # TODO: drop old pve2- schema with PVE 10
+    if ($rrd_dir_exists->("pve-node-9.0")) {
+        $data = $generate_rrd_string->(
+            [
+                $uptime,
+                $sublevel,
+                $ctime,
+                $avg1,
+                $maxcpu,
+                $stat->{cpu},
+                $stat->{wait},
+                $meminfo->{memtotal},
+                $meminfo->{memused},
+                $meminfo->{swaptotal},
+                $meminfo->{swapused},
+                $dinfo->{blocks},
+                $dused,
+                $netin,
+                $netout,
+                $meminfo->{memavailable},
+                $meminfo->{arcsize},
+                $pressures->{cpu}->{some}->{avg10},
+                $pressures->{io}->{some}->{avg10},
+                $pressures->{io}->{full}->{avg10},
+                $pressures->{memory}->{some}->{avg10},
+                $pressures->{memory}->{full}->{avg10},
+            ],
+        );
+        PVE::Cluster::broadcast_rrd("pve-node-9.0/$nodename", $data);
+    } else {
+        $data = $generate_rrd_string->(
+            [
+                $uptime,
+                $sublevel,
+                $ctime,
+                $avg1,
+                $maxcpu,
+                $stat->{cpu},
+                $stat->{wait},
+                $meminfo->{memtotal},
+                $meminfo->{memused},
+                $meminfo->{swaptotal},
+                $meminfo->{swapused},
+                $dinfo->{blocks},
+                $dused,
+                $netin,
+                $netout,
+            ],
+        );
+        PVE::Cluster::broadcast_rrd("pve2-node/$nodename", $data);
+    }
 
     my $node_metric = {
         uptime => $uptime,
@@ -273,44 +317,101 @@ sub update_qemu_status {
         my $data;
         my $status = $d->{qmpstatus} || $d->{status} || 'stopped';
         my $template = $d->{template} ? $d->{template} : "0";
-        if ($d->{pid}) { # running
-            $data = $generate_rrd_string->([
-                $d->{uptime},
-                $d->{name},
-                $status,
-                $template,
-                $ctime,
-                $d->{cpus},
-                $d->{cpu},
-                $d->{maxmem},
-                $d->{mem},
-                $d->{maxdisk},
-                $d->{disk},
-                $d->{netin},
-                $d->{netout},
-                $d->{diskread},
-                $d->{diskwrite},
-            ]);
+
+        # TODO: drop old pve2.3- schema with PVE 10
+        if ($rrd_dir_exists->("pve-vm-9.0")) {
+            if ($d->{pid}) { # running
+                $data = $generate_rrd_string->([
+                    $d->{uptime},
+                    $d->{name},
+                    $status,
+                    $template,
+                    $ctime,
+                    $d->{cpus},
+                    $d->{cpu},
+                    $d->{maxmem},
+                    $d->{mem},
+                    $d->{maxdisk},
+                    $d->{disk},
+                    $d->{netin},
+                    $d->{netout},
+                    $d->{diskread},
+                    $d->{diskwrite},
+                    $d->{memhost},
+                    $d->{pressurecpusome},
+                    $d->{pressurecpufull},
+                    $d->{pressureiosome},
+                    $d->{pressureiofull},
+                    $d->{pressurememorysome},
+                    $d->{pressurememoryfull},
+                ]);
+            } else {
+                $data = $generate_rrd_string->([
+                    0,
+                    $d->{name},
+                    $status,
+                    $template,
+                    $ctime,
+                    $d->{cpus},
+                    undef,
+                    $d->{maxmem},
+                    undef,
+                    $d->{maxdisk},
+                    $d->{disk},
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                ]);
+            }
+            PVE::Cluster::broadcast_rrd("pve-vm-9.0/$vmid", $data);
         } else {
-            $data = $generate_rrd_string->([
-                0,
-                $d->{name},
-                $status,
-                $template,
-                $ctime,
-                $d->{cpus},
-                undef,
-                $d->{maxmem},
-                undef,
-                $d->{maxdisk},
-                $d->{disk},
-                undef,
-                undef,
-                undef,
-                undef,
-            ]);
+            if ($d->{pid}) { # running
+                $data = $generate_rrd_string->([
+                    $d->{uptime},
+                    $d->{name},
+                    $status,
+                    $template,
+                    $ctime,
+                    $d->{cpus},
+                    $d->{cpu},
+                    $d->{maxmem},
+                    $d->{mem},
+                    $d->{maxdisk},
+                    $d->{disk},
+                    $d->{netin},
+                    $d->{netout},
+                    $d->{diskread},
+                    $d->{diskwrite},
+                ]);
+            } else {
+                $data = $generate_rrd_string->([
+                    0,
+                    $d->{name},
+                    $status,
+                    $template,
+                    $ctime,
+                    $d->{cpus},
+                    undef,
+                    $d->{maxmem},
+                    undef,
+                    $d->{maxdisk},
+                    $d->{disk},
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                ]);
+            }
+            PVE::Cluster::broadcast_rrd("pve2.3-vm/$vmid", $data);
         }
-        PVE::Cluster::broadcast_rrd("pve2.3-vm/$vmid", $data);
 
         PVE::ExtMetric::update_all($transactions, 'qemu', $vmid, $d, $ctime, $nodename);
     }
@@ -506,44 +607,100 @@ sub update_lxc_status {
         my $d = $vmstatus->{$vmid};
         my $template = $d->{template} ? $d->{template} : "0";
         my $data;
-        if ($d->{status} eq 'running') { # running
-            $data = $generate_rrd_string->([
-                $d->{uptime},
-                $d->{name},
-                $d->{status},
-                $template,
-                $ctime,
-                $d->{cpus},
-                $d->{cpu},
-                $d->{maxmem},
-                $d->{mem},
-                $d->{maxdisk},
-                $d->{disk},
-                $d->{netin},
-                $d->{netout},
-                $d->{diskread},
-                $d->{diskwrite},
-            ]);
+        # TODO: drop old pve2.3-vm schema with PVE 10
+        if ($rrd_dir_exists->("pve-vm-9.0")) {
+            if ($d->{pid}) { # running
+                $data = $generate_rrd_string->([
+                    $d->{uptime},
+                    $d->{name},
+                    $d->{status},
+                    $template,
+                    $ctime,
+                    $d->{cpus},
+                    $d->{cpu},
+                    $d->{maxmem},
+                    $d->{mem},
+                    $d->{maxdisk},
+                    $d->{disk},
+                    $d->{netin},
+                    $d->{netout},
+                    $d->{diskread},
+                    $d->{diskwrite},
+                    undef,
+                    $d->{pressurecpusome},
+                    $d->{pressurecpufull},
+                    $d->{pressureiosome},
+                    $d->{pressureiofull},
+                    $d->{pressurememorysome},
+                    $d->{pressurememoryfull},
+                ]);
+            } else {
+                $data = $generate_rrd_string->([
+                    0,
+                    $d->{name},
+                    $d->{status},
+                    $template,
+                    $ctime,
+                    $d->{cpus},
+                    undef,
+                    $d->{maxmem},
+                    undef,
+                    $d->{maxdisk},
+                    $d->{disk},
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                ]);
+            }
+            PVE::Cluster::broadcast_rrd("pve-vm-9.0/$vmid", $data);
         } else {
-            $data = $generate_rrd_string->([
-                0,
-                $d->{name},
-                $d->{status},
-                $template,
-                $ctime,
-                $d->{cpus},
-                undef,
-                $d->{maxmem},
-                undef,
-                $d->{maxdisk},
-                $d->{disk},
-                undef,
-                undef,
-                undef,
-                undef,
-            ]);
+            if ($d->{status} eq 'running') { # running
+                $data = $generate_rrd_string->([
+                    $d->{uptime},
+                    $d->{name},
+                    $d->{status},
+                    $template,
+                    $ctime,
+                    $d->{cpus},
+                    $d->{cpu},
+                    $d->{maxmem},
+                    $d->{mem},
+                    $d->{maxdisk},
+                    $d->{disk},
+                    $d->{netin},
+                    $d->{netout},
+                    $d->{diskread},
+                    $d->{diskwrite},
+                ]);
+            } else {
+                $data = $generate_rrd_string->([
+                    0,
+                    $d->{name},
+                    $d->{status},
+                    $template,
+                    $ctime,
+                    $d->{cpus},
+                    undef,
+                    $d->{maxmem},
+                    undef,
+                    $d->{maxdisk},
+                    $d->{disk},
+                    undef,
+                    undef,
+                    undef,
+                    undef,
+                ]);
+            }
+            PVE::Cluster::broadcast_rrd("pve2.3-vm/$vmid", $data);
         }
-        PVE::Cluster::broadcast_rrd("pve2.3-vm/$vmid", $data);
 
         PVE::ExtMetric::update_all($transactions, 'lxc', $vmid, $d, $ctime, $nodename);
     }
@@ -568,6 +725,7 @@ sub update_storage_status {
         my $data = $generate_rrd_string->([$ctime, $d->{total}, $d->{used}]);
 
         my $key = "pve2-storage/${nodename}/$storeid";
+        $key = "pve-storage-9.0/${nodename}/$storeid" if $rrd_dir_exists->("pve-storage-9.0");
         PVE::Cluster::broadcast_rrd($key, $data);
 
         PVE::ExtMetric::update_all($transactions, 'storage', $nodename, $storeid, $d, $ctime);
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 02/15] api: nodes: rrd and rrddata add decade option and use new pve-node-9.0 rrd files
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (9 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 01/15] pvestatd: collect and distribute new pve-{type}-9.0 metrics Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 03/15] api2tools: extract_vm_status add new vm memhost column Aaron Lauterer
                   ` (22 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

if the new rrd pve-node-9.0 files are present, they contain the current
data and should be used.

'decade' is now possible as timeframe with the new RRD format.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    changes since:
    RFC:
    * switch from pve9- to pve-{type}-9.0 schema

 PVE/API2/Nodes.pm | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm
index 1eb04d9a..69b3d873 100644
--- a/PVE/API2/Nodes.pm
+++ b/PVE/API2/Nodes.pm
@@ -836,7 +836,7 @@ __PACKAGE__->register_method({
             timeframe => {
                 description => "Specify the time frame you are interested in.",
                 type => 'string',
-                enum => ['hour', 'day', 'week', 'month', 'year'],
+                enum => ['hour', 'day', 'week', 'month', 'year', 'decade'],
             },
             ds => {
                 description => "The list of datasources you want to display.",
@@ -860,9 +860,10 @@ __PACKAGE__->register_method({
     code => sub {
         my ($param) = @_;
 
-        return PVE::RRD::create_rrd_graph(
-            "pve2-node/$param->{node}", $param->{timeframe}, $param->{ds}, $param->{cf},
-        );
+        my $path = "pve-node-9.0/$param->{node}";
+        $path = "pve2-node/$param->{node}" if !-e "/var/lib/rrdcached/db/${path}";
+        return PVE::RRD::create_rrd_graph($path, $param->{timeframe},
+            $param->{ds}, $param->{cf});
 
     },
 });
@@ -883,7 +884,7 @@ __PACKAGE__->register_method({
             timeframe => {
                 description => "Specify the time frame you are interested in.",
                 type => 'string',
-                enum => ['hour', 'day', 'week', 'month', 'year'],
+                enum => ['hour', 'day', 'week', 'month', 'year', 'decade'],
             },
             cf => {
                 description => "The RRD consolidation function",
@@ -903,8 +904,9 @@ __PACKAGE__->register_method({
     code => sub {
         my ($param) = @_;
 
-        return PVE::RRD::create_rrd_data("pve2-node/$param->{node}", $param->{timeframe},
-            $param->{cf});
+        my $path = "pve-node-9.0/$param->{node}";
+        $path = "pve2-node/$param->{node}" if !-e "/var/lib/rrdcached/db/${path}";
+        return PVE::RRD::create_rrd_data($path, $param->{timeframe}, $param->{cf});
     },
 });
 
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 03/15] api2tools: extract_vm_status add new vm memhost column
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (10 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 02/15] api: nodes: rrd and rrddata add decade option and use new pve-node-9.0 rrd files Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 04/15] ui: rrdmodels: add new columns and update existing Aaron Lauterer
                   ` (21 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

as this will also be displayed in the status of VMs

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    this is a dedicated patch that should be applied only for PVE9 as it
    adds new data in the result

 PVE/API2/Cluster.pm | 7 +++++++
 PVE/API2Tools.pm    | 3 +++
 2 files changed, 10 insertions(+)

diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm
index a025d264..81cdf217 100644
--- a/PVE/API2/Cluster.pm
+++ b/PVE/API2/Cluster.pm
@@ -301,6 +301,13 @@ __PACKAGE__->register_method({
                     renderer => 'bytes',
                     minimum => 0,
                 },
+                memhost => {
+                    description => "Used memory in bytes from the point of view of the host (for types 'qemu').",
+                    type => 'integer',
+                    optional => 1,
+                    renderer => 'bytes',
+                    minimum => 0,
+                },
                 maxmem => {
                     description => "Number of available memory in bytes"
                         . " (for types 'node', 'qemu' and 'lxc').",
diff --git a/PVE/API2Tools.pm b/PVE/API2Tools.pm
index 08548524..ed0bddbf 100644
--- a/PVE/API2Tools.pm
+++ b/PVE/API2Tools.pm
@@ -133,6 +133,9 @@ sub extract_vm_stats {
         $entry->{netout} = ($d->[12] || 0) + 0;
         $entry->{diskread} = ($d->[13] || 0) + 0;
         $entry->{diskwrite} = ($d->[14] || 0) + 0;
+        if ($key =~ /^pve-vm-/) {
+            $entry->{memhost} = ($d->[15] || 0) +0;
+        }
     }
 
     return $entry;
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 04/15] ui: rrdmodels: add new columns and update existing
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (11 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 03/15] api2tools: extract_vm_status add new vm memhost column Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 05/15] ui: node summary: use stacked memory graph with zfs arc Aaron Lauterer
                   ` (20 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

The new columns we get from RRD are added.

Since we are switching the memory graphs to stacked graphs, we need to
handle them a bit different because:
* gaps are not possible, we need to have a value, ideally 'null' when
  there is no data, makes it easier to handle in the tooltip
* calculate some values and not take the ones received from RRD.
  Otherwise the memory graphs can be _wobbly_. For example if we take
  the node memory where we have memused + arcsize + memavailable. Those
  will not always line up perfectly from the gathered data to match the
  total physical memory. Similar for the memory graph for guests.

The values we calculate are for nodes:
* memused-sub-arcsize: because the arcsize is included in memused, but
  we want to show it as a separate part of the graph if we do have that
  information.
  If we don't have the arcsize values (older node for example), we set
  it to 0.
* memfree-capped: instead of memavailable we calculate the free memory
  to avoid memory graphs that have wobbles and spikes due to timing
  differences when gathering the data.

For guests:
* memfree-capped: We cannot just have two line graphs, but will stack
  them as well to match the node graph. Therefore we need to subtract
  the memused from maxmen, so that in total, both data lines will result
  in maxmem.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    changes since:
    v3:
    * remove default values and handle situations where we need 'null' in
      the calculations. Only for guests we need to give 'mem' a default
      value as we use it directly in a stacked graph.
    * reorder calculation condition checks
    * rename calculated value for guests from maxmem-capped to
      memfree-capped to reflect better what it is
    v2:
    * add default values where needed for area graphs
    * add calculated values, usually to keep the memory graphs from spiking
      at the top due to slight timing differences where the data doesn't
      align perfectly
    RFC:
    * drop node memcache and membuffer columns as we now have memavailable
      which is better suited

 www/manager6/data/model/RRDModels.js | 44 +++++++++++++++++++++++++++-
 1 file changed, 43 insertions(+), 1 deletion(-)

diff --git a/www/manager6/data/model/RRDModels.js b/www/manager6/data/model/RRDModels.js
index 82f4e5cd..5070dac2 100644
--- a/www/manager6/data/model/RRDModels.js
+++ b/www/manager6/data/model/RRDModels.js
@@ -25,7 +25,33 @@ Ext.define('pve-rrd-node', {
         'rootused',
         'swaptotal',
         'swapused',
+        'memfree',
+        'arcsize',
+        'pressurecpusome',
+        'pressureiosome',
+        'pressureiofull',
+        'pressurememorysome',
+        'pressurememoryfull',
         { type: 'date', dateFormat: 'timestamp', name: 'time' },
+        {
+            name: 'memfree-capped',
+            calculate: function (data) {
+                if (data.memtotal >= 0 && data.memused >= 0 && data.memtotal >= data.memused) {
+                    return data.memtotal - data.memused;
+                }
+                return null;
+            },
+        },
+        {
+            name: 'memused-sub-arcsize',
+            calculate: function (data) {
+                let arcsize = data.arcsize ?? 0; // pre pve9 nodes don't report any arcsize
+                if (data.memused >= 0 && arcsize >= 0 && data.memused >= arcsize) {
+                    return data.memused - arcsize;
+                }
+                return null;
+            },
+        },
     ],
 });
 
@@ -42,13 +68,29 @@ Ext.define('pve-rrd-guest', {
         'maxcpu',
         'netin',
         'netout',
-        'mem',
+        { name: 'mem', defaultValue: null },
         'maxmem',
         'disk',
         'maxdisk',
         'diskread',
         'diskwrite',
+        'memhost',
+        'pressurecpusome',
+        'pressurecpufull',
+        'pressureiosome',
+        'pressurecpufull',
+        'pressurememorysome',
+        'pressurememoryfull',
         { type: 'date', dateFormat: 'timestamp', name: 'time' },
+        {
+            name: 'memfree-capped',
+            calculate: function (data) {
+                if (data.maxmem >= 0 && data.mem >= 0 && data.maxmem >= data.mem) {
+                    return data.maxmem - data.mem;
+                }
+                return null;
+            },
+        },
     ],
 });
 
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 05/15] ui: node summary: use stacked memory graph with zfs arc
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (12 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 04/15] ui: rrdmodels: add new columns and update existing Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 06/15] ui: add pressure graphs to node and guest summary Aaron Lauterer
                   ` (19 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

To display the used memory and the ZFS arc as a separate data point,
keeping the old line overlapping filled line graphs won't work
anymore. We therefore switch them to area graphs which are stacked by
default.

The order of the fields is important here as it affects the order in the
stacking. This means we also need to override colors manually to keep
them in line as it used to be.
Additionally, we don't use the 3rd color in the default extjs color
scheme, as that would be dark red [0]. We go with a color that is
different enough and not associated as a warning or error: dark-grey.

[0] https://docs.sencha.com/extjs/7.0.0/classic/src/Base.js-6.html#line318

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/node/Summary.js | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/www/manager6/node/Summary.js b/www/manager6/node/Summary.js
index c9d73494..ed3d33d9 100644
--- a/www/manager6/node/Summary.js
+++ b/www/manager6/node/Summary.js
@@ -177,11 +177,18 @@ Ext.define('PVE.node.Summary', {
                         {
                             xtype: 'proxmoxRRDChart',
                             title: gettext('Memory usage'),
-                            fields: ['memtotal', 'memused'],
-                            fieldTitles: [gettext('Total'), gettext('RAM usage')],
+                            fields: [
+                                {
+                                    type: 'area',
+                                    yField: ['memused-sub-arcsize', 'arcsize', 'memfree-capped'],
+                                    title: [gettext('Used'), gettext('ZFS'), gettext('Free')],
+                                },
+                            ],
+                            colors: ['#115fa6', '#7c7474', '#94ae0a'],
                             unit: 'bytes',
                             powerOfTwo: true,
                             store: rrdstore,
+                            stacked: true,
                         },
                         {
                             xtype: 'proxmoxRRDChart',
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 06/15] ui: add pressure graphs to node and guest summary
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (13 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 05/15] ui: node summary: use stacked memory graph with zfs arc Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 07/15] ui: GuestStatusView: add memhost for VM guests Aaron Lauterer
                   ` (18 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

From: Folke Gleumes <f.gleumes@proxmox.com>

Pressures are indicatios that processes needed to wait for their
resources. While 'some' means, that some of the processes on the host
(node summary) or in the guests cgroup had to wait, 'full' means that
all processes couldn't get the resources fast enough.

We set the colors accordingly. For 'some' we use yellow, for 'full' we
use red.
This should make it clear that this is not just another graph, but
indicates performance issues. It also sets the pressure graphs apart
from the other graphs that follow the usual color scheme.

Originally-by: Folke Gleumes <f.gleumes@proxmox.com>
[AL:
    * rebased
    * reworked commit msg
    * set colors
]
Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/node/Summary.js       | 27 +++++++++++++++++++++++++++
 www/manager6/panel/GuestSummary.js | 30 ++++++++++++++++++++++++++++++
 2 files changed, 57 insertions(+)

diff --git a/www/manager6/node/Summary.js b/www/manager6/node/Summary.js
index ed3d33d9..b00fcf2e 100644
--- a/www/manager6/node/Summary.js
+++ b/www/manager6/node/Summary.js
@@ -196,6 +196,33 @@ Ext.define('PVE.node.Summary', {
                             fields: ['netin', 'netout'],
                             store: rrdstore,
                         },
+                        {
+                            xtype: 'proxmoxRRDChart',
+                            title: gettext('CPU pressure'),
+                            fieldTitles: ['Some'],
+                            fields: ['pressurecpusome'],
+                            colors: ['#FFD13E', '#A61120'],
+                            store: rrdstore,
+                            unit: 'percent',
+                        },
+                        {
+                            xtype: 'proxmoxRRDChart',
+                            title: gettext('IO pressure'),
+                            fieldTitles: ['Some', 'Full'],
+                            fields: ['pressureiosome', 'pressureiofull'],
+                            colors: ['#FFD13E', '#A61120'],
+                            store: rrdstore,
+                            unit: 'percent',
+                        },
+                        {
+                            xtype: 'proxmoxRRDChart',
+                            title: gettext('Memory pressure'),
+                            fieldTitles: ['Some', 'Full'],
+                            fields: ['pressurememorysome', 'pressurememoryfull'],
+                            colors: ['#FFD13E', '#A61120'],
+                            store: rrdstore,
+                            unit: 'percent',
+                        },
                     ],
                     listeners: {
                         resize: function (panel) {
diff --git a/www/manager6/panel/GuestSummary.js b/www/manager6/panel/GuestSummary.js
index 5efbe40f..0b62dbb7 100644
--- a/www/manager6/panel/GuestSummary.js
+++ b/www/manager6/panel/GuestSummary.js
@@ -102,6 +102,36 @@ Ext.define('PVE.guest.Summary', {
                     fields: ['diskread', 'diskwrite'],
                     store: rrdstore,
                 },
+                {
+                    xtype: 'proxmoxRRDChart',
+                    title: gettext('CPU pressure'),
+                    pveSelNode: me.pveSelNode,
+                    fieldTitles: ['Some', 'Full'],
+                    fields: ['pressurecpusome', 'pressurecpufull'],
+                    colors: ['#FFD13E', '#A61120'],
+                    store: rrdstore,
+                    unit: 'percent',
+                },
+                {
+                    xtype: 'proxmoxRRDChart',
+                    title: gettext('IO pressure'),
+                    pveSelNode: me.pveSelNode,
+                    fieldTitles: ['Some', 'Full'],
+                    fields: ['pressureiosome', 'pressureiofull'],
+                    colors: ['#FFD13E', '#A61120'],
+                    store: rrdstore,
+                    unit: 'percent',
+                },
+                {
+                    xtype: 'proxmoxRRDChart',
+                    title: gettext('Memory pressure'),
+                    pveSelNode: me.pveSelNode,
+                    fieldTitles: ['Some', 'Full'],
+                    fields: ['pressurememorysome', 'pressurememoryfull'],
+                    colors: ['#FFD13E', '#A61120'],
+                    store: rrdstore,
+                    unit: 'percent',
+                },
             );
         }
 
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 07/15] ui: GuestStatusView: add memhost for VM guests
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (14 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 06/15] ui: add pressure graphs to node and guest summary Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 08/15] ui: GuestSummary: memory switch to stacked and add hostmem Aaron Lauterer
                   ` (17 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

With the new memhost field, the vertical space is getting tight. We
therefore reduce the height of the separator boxes.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/panel/GuestStatusView.js | 18 ++++++++++++++++--
 1 file changed, 16 insertions(+), 2 deletions(-)

diff --git a/www/manager6/panel/GuestStatusView.js b/www/manager6/panel/GuestStatusView.js
index 0134526c..3369f7b3 100644
--- a/www/manager6/panel/GuestStatusView.js
+++ b/www/manager6/panel/GuestStatusView.js
@@ -94,7 +94,7 @@ Ext.define('PVE.panel.GuestStatusView', {
         },
         {
             xtype: 'box',
-            height: 15,
+            height: 10,
         },
         {
             itemId: 'cpu',
@@ -114,6 +114,20 @@ Ext.define('PVE.panel.GuestStatusView', {
             valueField: 'mem',
             maxField: 'maxmem',
         },
+        {
+            itemId: 'memory-host',
+            iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon',
+            title: gettext('Host memory usage'),
+            valueField: 'memhost',
+            printBar: false,
+            renderer: function (used, max) {
+                return Proxmox.Utils.render_size(used);
+            },
+            cbind: {
+                hidden: '{isLxc}',
+                disabled: '{isLxc}',
+            },
+        },
         {
             itemId: 'swap',
             iconCls: 'fa fa-refresh fa-fw',
@@ -144,7 +158,7 @@ Ext.define('PVE.panel.GuestStatusView', {
         },
         {
             xtype: 'box',
-            height: 15,
+            height: 10,
         },
         {
             itemId: 'ips',
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 08/15] ui: GuestSummary: memory switch to stacked and add hostmem
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (15 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 07/15] ui: GuestStatusView: add memhost for VM guests Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 09/15] ui: GuestSummary: remember visibility of host memory view Aaron Lauterer
                   ` (16 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

We switch the memory graph to a stacked area graph, similar to what we
have now on the node summary page.

Since the order is important, we need to define the colors manually, as
the default color scheme would switch the colors as we usually have
them.

Additionally we add the host memory view as another data series. But we
keep it as a single line without fill. We chose the grey tone so that is
works for both, bright and dark theme.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    changes since:
    v3:
    * align column from maxmem-capped to memfree-capped
    * rename columns:
      * 'RAM Used' -> 'Used'
      * 'Configured' -> 'Free'

 www/manager6/panel/GuestSummary.js | 25 +++++++++++++++++++++++--
 1 file changed, 23 insertions(+), 2 deletions(-)

diff --git a/www/manager6/panel/GuestSummary.js b/www/manager6/panel/GuestSummary.js
index 0b62dbb7..77ec3e3f 100644
--- a/www/manager6/panel/GuestSummary.js
+++ b/www/manager6/panel/GuestSummary.js
@@ -30,6 +30,27 @@ Ext.define('PVE.guest.Summary', {
         var template = !!me.pveSelNode.data.template;
         var rstore = me.statusStore;
 
+        let memoryFields = [
+            {
+                type: 'area',
+                yField: ['mem', 'memfree-capped'],
+                title: [gettext('Used'), gettext('Free')],
+            },
+        ];
+        if (type === 'qemu') {
+            memoryFields.push({
+                type: 'line',
+                fill: false,
+                yField: 'memhost',
+                title: gettext('Host memory usage'),
+                hidden: true,
+                style: {
+                    lineWidth: 2.5,
+                    opacity: 1,
+                },
+            });
+        }
+
         var items = [
             {
                 xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView',
@@ -82,8 +103,8 @@ Ext.define('PVE.guest.Summary', {
                     xtype: 'proxmoxRRDChart',
                     title: gettext('Memory usage'),
                     pveSelNode: me.pveSelNode,
-                    fields: ['maxmem', 'mem'],
-                    fieldTitles: [gettext('Total'), gettext('RAM usage')],
+                    fields: memoryFields,
+                    colors: ['#115fa6', '#94ae0a', '#c4c0c0'],
                     unit: 'bytes',
                     powerOfTwo: true,
                     store: rrdstore,
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 09/15] ui: GuestSummary: remember visibility of host memory view
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (16 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 08/15] ui: GuestSummary: memory switch to stacked and add hostmem Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 10/15] ui: nodesummary: guestsummary: add tooltip info buttons Aaron Lauterer
                   ` (15 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

by utilizing the itemclick event of a charts legend and storing it as
state.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    this could potentially be squashed with (ui: GuestSummary: memory switch
            to stacked and add hostmem)

 www/manager6/panel/GuestSummary.js | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/www/manager6/panel/GuestSummary.js b/www/manager6/panel/GuestSummary.js
index 77ec3e3f..0f0bae23 100644
--- a/www/manager6/panel/GuestSummary.js
+++ b/www/manager6/panel/GuestSummary.js
@@ -30,6 +30,9 @@ Ext.define('PVE.guest.Summary', {
         var template = !!me.pveSelNode.data.template;
         var rstore = me.statusStore;
 
+        let hideMemhostStateKey = 'pve-vm-hide-memhost';
+        let sp = Ext.state.Manager.getProvider();
+
         let memoryFields = [
             {
                 type: 'area',
@@ -43,7 +46,7 @@ Ext.define('PVE.guest.Summary', {
                 fill: false,
                 yField: 'memhost',
                 title: gettext('Host memory usage'),
-                hidden: true,
+                hidden: sp.get(hideMemhostStateKey, true),
                 style: {
                     lineWidth: 2.5,
                     opacity: 1,
@@ -108,6 +111,12 @@ Ext.define('PVE.guest.Summary', {
                     unit: 'bytes',
                     powerOfTwo: true,
                     store: rrdstore,
+                    onLegendChange: function (_legend, record, _, seriesIndex) {
+                        if (seriesIndex === 2) {
+                            // third data series is clicked -> hostmem
+                            sp.set(hideMemhostStateKey, record.data.disabled);
+                        }
+                    },
                 },
                 {
                     xtype: 'proxmoxRRDChart',
@@ -185,7 +194,6 @@ Ext.define('PVE.guest.Summary', {
             rrdstore.startUpdate();
             me.on('destroy', rrdstore.stopUpdate);
         }
-        let sp = Ext.state.Manager.getProvider();
         me.mon(sp, 'statechange', function (provider, key, value) {
             if (key !== 'summarycolumns') {
                 return;
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 10/15] ui: nodesummary: guestsummary: add tooltip info buttons
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (17 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 09/15] ui: GuestSummary: remember visibility of host memory view Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 11/15] ui: summaries: use titles for disk and network series Aaron Lauterer
                   ` (14 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

This way, we can provide a bit more context to what the graph is
showing. Hopefully making it easier for our users to draw useful
conclusions from the provided information.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    while not available for all graphs for now, this should help users
    understand the more complex ones.
    
    The phrasing might be improved of course.

 www/manager6/node/Summary.js       | 40 ++++++++++++++++++++++++++++++
 www/manager6/panel/GuestSummary.js | 36 +++++++++++++++++++++++++++
 2 files changed, 76 insertions(+)

diff --git a/www/manager6/node/Summary.js b/www/manager6/node/Summary.js
index b00fcf2e..7bd3324c 100644
--- a/www/manager6/node/Summary.js
+++ b/www/manager6/node/Summary.js
@@ -162,6 +162,16 @@ Ext.define('PVE.node.Summary', {
                         {
                             xtype: 'proxmoxRRDChart',
                             title: gettext('CPU usage'),
+                            tools: [
+                                {
+                                    glyph: 'xf05a@FontAwesome', // fa-info-circle
+                                    tooltip: gettext("IO Delay is a measure of how much time processes had to wait for IO to be finished."),
+                                    disabled: false,
+                                    style: {
+                                        paddingRight: '5px',
+                                    },
+                                },
+                            ],
                             fields: ['cpu', 'iowait'],
                             fieldTitles: [gettext('CPU usage'), gettext('IO delay')],
                             unit: 'percent',
@@ -199,6 +209,16 @@ Ext.define('PVE.node.Summary', {
                         {
                             xtype: 'proxmoxRRDChart',
                             title: gettext('CPU pressure'),
+                            tools: [
+                                {
+                                    glyph: 'xf05a@FontAwesome', // fa-info-circle
+                                    tooltip: gettext("Shows if some processes on the host had to wait for CPU resources."),
+                                    disabled: false,
+                                    style: {
+                                        paddingRight: '5px',
+                                    },
+                                },
+                            ],
                             fieldTitles: ['Some'],
                             fields: ['pressurecpusome'],
                             colors: ['#FFD13E', '#A61120'],
@@ -208,6 +228,16 @@ Ext.define('PVE.node.Summary', {
                         {
                             xtype: 'proxmoxRRDChart',
                             title: gettext('IO pressure'),
+                            tools: [
+                                {
+                                    glyph: 'xf05a@FontAwesome', // fa-info-circle
+                                    tooltip: gettext("Shows if some or all (Full) processes on the host had to wait for IO (disk & network) resources."),
+                                    disabled: false,
+                                    style: {
+                                        paddingRight: '5px',
+                                    },
+                                },
+                            ],
                             fieldTitles: ['Some', 'Full'],
                             fields: ['pressureiosome', 'pressureiofull'],
                             colors: ['#FFD13E', '#A61120'],
@@ -217,6 +247,16 @@ Ext.define('PVE.node.Summary', {
                         {
                             xtype: 'proxmoxRRDChart',
                             title: gettext('Memory pressure'),
+                            tools: [
+                                {
+                                    glyph: 'xf05a@FontAwesome', // fa-info-circle
+                                    tooltip: gettext("Shows if some or all (Full) processes on the host had to wait for memory resources."),
+                                    disabled: false,
+                                    style: {
+                                        paddingRight: '5px',
+                                    },
+                                },
+                            ],
                             fieldTitles: ['Some', 'Full'],
                             fields: ['pressurememorysome', 'pressurememoryfull'],
                             colors: ['#FFD13E', '#A61120'],
diff --git a/www/manager6/panel/GuestSummary.js b/www/manager6/panel/GuestSummary.js
index 0f0bae23..3f12c6e0 100644
--- a/www/manager6/panel/GuestSummary.js
+++ b/www/manager6/panel/GuestSummary.js
@@ -135,6 +135,18 @@ Ext.define('PVE.guest.Summary', {
                 {
                     xtype: 'proxmoxRRDChart',
                     title: gettext('CPU pressure'),
+                    tools: [
+                        {
+                            glyph: 'xf05a@FontAwesome', // fa-info-circle
+                            tooltip: gettext(
+                                'Shows if some or all (Full) processes belonging to the guest had to wait for CPU resources.',
+                            ),
+                            disabled: false,
+                            style: {
+                                paddingRight: '5px',
+                            },
+                        },
+                    ],
                     pveSelNode: me.pveSelNode,
                     fieldTitles: ['Some', 'Full'],
                     fields: ['pressurecpusome', 'pressurecpufull'],
@@ -145,6 +157,18 @@ Ext.define('PVE.guest.Summary', {
                 {
                     xtype: 'proxmoxRRDChart',
                     title: gettext('IO pressure'),
+                    tools: [
+                        {
+                            glyph: 'xf05a@FontAwesome', // fa-info-circle
+                            tooltip: gettext(
+                                'Shows if some or all (Full) processes belonging to the guest had to wait for IO (disk & network) resources.',
+                            ),
+                            disabled: false,
+                            style: {
+                                paddingRight: '5px',
+                            },
+                        },
+                    ],
                     pveSelNode: me.pveSelNode,
                     fieldTitles: ['Some', 'Full'],
                     fields: ['pressureiosome', 'pressureiofull'],
@@ -155,6 +179,18 @@ Ext.define('PVE.guest.Summary', {
                 {
                     xtype: 'proxmoxRRDChart',
                     title: gettext('Memory pressure'),
+                    tools: [
+                        {
+                            glyph: 'xf05a@FontAwesome', // fa-info-circle
+                            tooltip: gettext(
+                                'Shows if some or all (Full) processes belonging to the guest had to wait for memory resources.',
+                            ),
+                            disabled: false,
+                            style: {
+                                paddingRight: '5px',
+                            },
+                        },
+                    ],
                     pveSelNode: me.pveSelNode,
                     fieldTitles: ['Some', 'Full'],
                     fields: ['pressurememorysome', 'pressurememoryfull'],
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 11/15] ui: summaries: use titles for disk and network series
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (18 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 10/15] ui: nodesummary: guestsummary: add tooltip info buttons Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 12/15] fix #6068: ui: utils: calculate and render host memory usage correctly Aaron Lauterer
                   ` (13 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

They were missing and just showed the actual field names.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/node/Summary.js       | 1 +
 www/manager6/panel/GuestSummary.js | 2 ++
 2 files changed, 3 insertions(+)

diff --git a/www/manager6/node/Summary.js b/www/manager6/node/Summary.js
index 7bd3324c..a7e54bf4 100644
--- a/www/manager6/node/Summary.js
+++ b/www/manager6/node/Summary.js
@@ -204,6 +204,7 @@ Ext.define('PVE.node.Summary', {
                             xtype: 'proxmoxRRDChart',
                             title: gettext('Network traffic'),
                             fields: ['netin', 'netout'],
+                            fieldTitles: [gettext('Incoming'), gettext('Outgoing')],
                             store: rrdstore,
                         },
                         {
diff --git a/www/manager6/panel/GuestSummary.js b/www/manager6/panel/GuestSummary.js
index 3f12c6e0..f79f3458 100644
--- a/www/manager6/panel/GuestSummary.js
+++ b/www/manager6/panel/GuestSummary.js
@@ -123,6 +123,7 @@ Ext.define('PVE.guest.Summary', {
                     title: gettext('Network traffic'),
                     pveSelNode: me.pveSelNode,
                     fields: ['netin', 'netout'],
+                    fieldTitles: [gettext('Incoming'), gettext('Outgoing')],
                     store: rrdstore,
                 },
                 {
@@ -130,6 +131,7 @@ Ext.define('PVE.guest.Summary', {
                     title: gettext('Disk IO'),
                     pveSelNode: me.pveSelNode,
                     fields: ['diskread', 'diskwrite'],
+                    fieldTitles: [gettext('Reads'), gettext('Writes')],
                     store: rrdstore,
                 },
                 {
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 12/15] fix #6068: ui: utils: calculate and render host memory usage correctly
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (19 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 11/15] ui: summaries: use titles for disk and network series Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 13/15] d/control: require proxmox-rrd-migration-tool >= 1.0.0 Aaron Lauterer
                   ` (12 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

by adding the new memhost field, which is populated for VMs, and
using it if the guest is of type qemu and the field is numerical.

As a result, if the cluster is in a mixed PVE8 / PVE9 situation, for
example during a migration, we will not report any host memory usage, in
numbers or percent, as we don't get the memhost metric from the older
PVE8 hosts.

Fixes: #6068 (Node Search tab incorrect Host memory usage %)
Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
 www/manager6/Utils.js              | 6 ++++++
 www/manager6/data/ResourceStore.js | 8 ++++++++
 2 files changed, 14 insertions(+)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index e403db40..57a818f6 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1160,6 +1160,9 @@ Ext.define('PVE.Utils', {
                 return -1;
             }
 
+            if (data.type === 'qemu' && Ext.isNumeric(data.memhost)) {
+                return data.memhost / maxmem;
+            }
             return data.mem / maxmem;
         },
 
@@ -1203,6 +1206,9 @@ Ext.define('PVE.Utils', {
             if (record.data.mem > 1) {
                 // we got no percentage but bytes
                 let mem = record.data.mem;
+                if (record.data.type === 'qemu' && Ext.isNumeric(record.data.memhost)) {
+                    mem = record.data.memhost;
+                }
                 if (!record.data.uptime || maxmem === 0 || !Ext.isNumeric(mem)) {
                     return '';
                 }
diff --git a/www/manager6/data/ResourceStore.js b/www/manager6/data/ResourceStore.js
index 13af3b4e..d1f3fb63 100644
--- a/www/manager6/data/ResourceStore.js
+++ b/www/manager6/data/ResourceStore.js
@@ -167,6 +167,14 @@ Ext.define('PVE.data.ResourceStore', {
                 hidden: true,
                 width: 100,
             },
+            memhost: {
+                header: gettext('Host Memory usage'),
+                type: 'integer',
+                renderer: PVE.Utils.render_mem_usage,
+                sortable: true,
+                hidden: true,
+                width: 100,
+            },
             memuse: {
                 header: gettext('Memory usage') + ' %',
                 type: 'number',
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 13/15] d/control: require proxmox-rrd-migration-tool >= 1.0.0
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (20 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 12/15] fix #6068: ui: utils: calculate and render host memory usage correctly Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 14/15] d/postinst: run promox-rrd-migration-tool Aaron Lauterer
                   ` (11 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
 debian/control | 1 +
 1 file changed, 1 insertion(+)

diff --git a/debian/control b/debian/control
index ffac171c..d1985f65 100644
--- a/debian/control
+++ b/debian/control
@@ -83,6 +83,7 @@ Depends: apt (>= 1.5~),
          postfix | mail-transport-agent,
          proxmox-mail-forward,
          proxmox-mini-journalreader (>= 1.3-1),
+         proxmox-rrd-migration-tool (>= 1.0.0),
          proxmox-widget-toolkit (>= 5.0.2),
          pve-cluster (>= 9.0.1),
          pve-container (>= 5.2.5),
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager v4 14/15] d/postinst: run promox-rrd-migration-tool
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (21 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 13/15] d/control: require proxmox-rrd-migration-tool >= 1.0.0 Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-29 12:09   ` Lukas Wagner
  2025-07-26  1:06 ` [pve-devel] [PATCH manager stabe-8+master v4 15/15] pve8to9: add checkfs for RRD migration Aaron Lauterer
                   ` (10 subsequent siblings)
  33 siblings, 1 reply; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    currently it checks for lt 9.0.0~12. should it only be applied to a
    later version, don't forget to adapt the version check!
    
    I tested it by bumping the version to 9.0.0~12
    upgraded to it -> migration ran
    reinstalled -> no migration happening
    
    when installing the bumped pve-manager package and the
    proxmox-rrd-migration-tool package at the same time, dependencies are
    resolved and the postinst script works.
    
    There is still one bug though that happens on my live system: While the
    migration tool moves the processed files to FILE.old, new ones without
    the .old are still present.
    I did a quick try, disabling rrdached before we call the migration tool.
    But that didn't help. Could be that pmxcfs is receiving new data and is
    recreating them. Or maybe something else.
    That would need to be debugged to figure out as apparently I did miss
    something here regarding the behavior.

 debian/postinst | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/debian/postinst b/debian/postinst
index a0480b24..b15603ac 100755
--- a/debian/postinst
+++ b/debian/postinst
@@ -227,6 +227,11 @@ case "$1" in
             migrate_apt_auth_conf
         fi
     fi
+
+    if test -n "$2" && dpkg --compare-versions "$2" 'lt' '9.0.0~12'; then
+        echo "migradting RRD to new PVE format version - this can take some time!"
+        proxmox-rrd-migration-tool --migrate || echo "migration failed, see output above for errors and try to migrate existing data manually by running 'proxmox-rrd-migration-tool --migrate'"
+    fi
     ;;
 
   abort-upgrade|abort-remove|abort-deconfigure)
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH manager stabe-8+master v4 15/15] pve8to9: add checkfs for RRD migration
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (22 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 14/15] d/postinst: run promox-rrd-migration-tool Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-29  8:15   ` Lukas Wagner
  2025-07-26  1:06 ` [pve-devel] [PATCH storage v4 1/1] status: rrddata: use new pve-storage-9.0 rrd location if file is present Aaron Lauterer
                   ` (9 subsequent siblings)
  33 siblings, 1 reply; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

As the new RRD files are quite a bit larger than the old ones, we should
check if the estimated required space is actually available and let the
users know if not.

Secondly, it could be possible that a new resource is added while the
node is migrating the RRD files. Therefore, there could be some left not
migrated to the new format. Therefore, check for that too and let the
user know how they can migrate the remaining RRD files.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
 PVE/CLI/pve8to9.pm | 62 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)

diff --git a/PVE/CLI/pve8to9.pm b/PVE/CLI/pve8to9.pm
index fec89a7c..6107cc60 100644
--- a/PVE/CLI/pve8to9.pm
+++ b/PVE/CLI/pve8to9.pm
@@ -1714,6 +1714,67 @@ sub check_bridge_mtu {
     }
 }
 
+sub check_rrd_migration {
+    if (-e "/var/lib/rrdcached/db/pve-node-9.0") {
+        log_info("Check post RRD migration situation...");
+
+        my $count = 0;
+        my $count_occurences = sub {
+            $count++;
+        };
+        eval {
+            run_command(
+                ['find /var/lib/rrdcached/db -type f ! -name "*.foo"'],
+                outfunc => $count_occurences,
+                noerr => 1,
+            );
+        };
+
+        if ($count) {
+            log_warn("Found '$count' RRD files that have not yet been migrated to the new schema."
+                . " Please run the following command manually:\n"
+                . "proxmox-rrd-migration-tool --migrate\n");
+        }
+
+    } else {
+        log_info("Check space requirements for RRD migration...");
+        # multiplier values taken from KiB sizes of old and new RRD files
+        my $rrd_dirs = {
+            nodes => {
+                path => "/var/lib/rrdcached/db/pve2-node",
+                multiplier => 18.1,
+            },
+            guests => {
+                path => "/var/lib/rrdcached/db/pve2-vm",
+                multiplier => 20.2,
+            },
+            storage => {
+                path => "/var/lib/rrdcached/db/pve2-storage",
+                multiplier => 11.14,
+            },
+        };
+
+        my $size_buffer = 1024 * 1024 * 1024; # at least one GiB of free space should be calculated in
+        my $total_size_estimate = 0;
+        for my $type (keys %$rrd_dirs) {
+            my $size = PVE::Tools::du($rrd_dirs->{$type}->{path});
+            $total_size_estimate =
+                $total_size_estimate + ($size * $rrd_dirs->{$type}->{multiplier});
+        }
+        my $root_free = PVE::Tools::df('/', 10);
+
+        if (($total_size_estimate + $size_buffer) >= $root_free->{avail}) {
+            my $estimate_gib = sprintf("%.2f", $total_size_estimate / 1024 / 1024 / 1024);
+            my $free_gib = sprintf("%.2f", $root_free->{avail} / 1024 / 1024 / 1024);
+
+            log_fail("Not enough free space to migrate existing RRD files to the new format!\n"
+                . "Migrating the current RRD files is expected to consume about ${estimate_gib} GiB plus 1 GiB of safety."
+                . " But there is currently only ${free_gib} GiB space on the root file system available.\n"
+            );
+        }
+    }
+}
+
 sub check_virtual_guests {
     print_header("VIRTUAL GUEST CHECKS");
 
@@ -1882,6 +1943,7 @@ sub check_misc {
     check_legacy_notification_sections();
     check_legacy_backup_job_options();
     check_lvm_autoactivation();
+    check_rrd_migration();
 }
 
 my sub colored_if {
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH storage v4 1/1] status: rrddata: use new pve-storage-9.0 rrd location if file is present
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (23 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH manager stabe-8+master v4 15/15] pve8to9: add checkfs for RRD migration Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 1/4] metrics: add pressure to metrics Aaron Lauterer
                   ` (8 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    changes since:
    RFC:
    * switch from pve9-storage to pve-storage-90 schema

 src/PVE/API2/Storage/Status.pm | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/PVE/API2/Storage/Status.pm b/src/PVE/API2/Storage/Status.pm
index c172073..ad8c753 100644
--- a/src/PVE/API2/Storage/Status.pm
+++ b/src/PVE/API2/Storage/Status.pm
@@ -415,11 +415,10 @@ __PACKAGE__->register_method({
     code => sub {
         my ($param) = @_;
 
-        return PVE::RRD::create_rrd_data(
-            "pve2-storage/$param->{node}/$param->{storage}",
-            $param->{timeframe},
-            $param->{cf},
-        );
+        my $path = "pve-storage-9.0/$param->{node}/$param->{storage}";
+        $path = "pve2-storage/$param->{node}/$param->{storage}"
+            if !-e "/var/lib/rrdcached/db/${path}";
+        return PVE::RRD::create_rrd_data($path, $param->{timeframe}, $param->{cf});
     },
 });
 
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH qemu-server v4 1/4] metrics: add pressure to metrics
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (24 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH storage v4 1/1] status: rrddata: use new pve-storage-9.0 rrd location if file is present Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 2/4] vmstatus: add memhost for host view of vm mem consumption Aaron Lauterer
                   ` (7 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

From: Folke Gleumes <f.gleumes@proxmox.com>

Originally-by: Folke Gleumes <f.gleumes@proxmox.com>
[AL:
    * rebased on current master
    * switch to new, more generic read_cgroup_pressure function
    * add pressures to return properties
]
Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    changes since:
    v2:
    * added return properties
    * reordered collection prior to the cpu collection, as it would be
      skipped, especially when collected via `pvesh`
    * added '* 1' to make sure we use numbers in the JSON -> an better
      alternative for numbers that are not integers?

 src/PVE/QemuServer.pm | 39 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 5a4f8120..59b13709 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -2533,6 +2533,36 @@ our $vmstatus_return_properties = {
         type => 'boolean',
         optional => 1,
     },
+    pressurecpusome => {
+        description => "CPU Some pressure average over the last 10 seconds.",
+        type => 'number',
+        optional => 1,
+    },
+    pressurecpufull => {
+        description => "CPU Full pressure average over the last 10 seconds.",
+        type => 'number',
+        optional => 1,
+    },
+    pressureiosome => {
+        description => "IO Some pressure average over the last 10 seconds.",
+        type => 'number',
+        optional => 1,
+    },
+    pressureiofull => {
+        description => "IO Full pressure average over the last 10 seconds.",
+        type => 'number',
+        optional => 1,
+    },
+    pressurememorysome => {
+        description => "Memory Some pressure average over the last 10 seconds.",
+        type => 'number',
+        optional => 1,
+    },
+    pressurememoryfull => {
+        description => "Memory Full pressure average over the last 10 seconds.",
+        type => 'number',
+        optional => 1,
+    },
 };
 
 my $last_proc_pid_stat;
@@ -2645,6 +2675,14 @@ sub vmstatus {
             $d->{mem} = int(($pstat->{rss} / $pstat->{vsize}) * $d->{maxmem});
         }
 
+        my $pressures = PVE::ProcFSTools::read_cgroup_pressure("qemu.slice/${vmid}.scope");
+        $d->{pressurecpusome} = $pressures->{cpu}->{some}->{avg10} * 1;
+        $d->{pressurecpufull} = $pressures->{cpu}->{full}->{avg10} * 1;
+        $d->{pressureiosome} = $pressures->{io}->{some}->{avg10} * 1;
+        $d->{pressureiofull} = $pressures->{io}->{full}->{avg10} * 1;
+        $d->{pressurememorysome} = $pressures->{memory}->{some}->{avg10} * 1;
+        $d->{pressurememoryfull} = $pressures->{memory}->{full}->{avg10} * 1;
+
         my $old = $last_proc_pid_stat->{$pid};
         if (!$old) {
             $last_proc_pid_stat->{$pid} = {
@@ -2669,6 +2707,7 @@ sub vmstatus {
         } else {
             $d->{cpu} = $old->{cpu};
         }
+
     }
 
     return $res if !$full;
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH qemu-server v4 2/4] vmstatus: add memhost for host view of vm mem consumption
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (25 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 1/4] metrics: add pressure to metrics Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-29 12:49   ` Lukas Wagner
  2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 3/4] vmstatus: switch mem stat to PSS of VM cgroup Aaron Lauterer
                   ` (6 subsequent siblings)
  33 siblings, 1 reply; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

The mem field itself will switch from the outside view to the "inside"
view if the VM is reporting detailed memory usage informatio via the
ballooning device.

Since sometimes other processes belong to a VM too, for example swtpm,
we collect all PIDs belonging to the VM cgroup and fetch their PSS data
to account for shared libraries used.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    changes since:
    v2:
    * add memhost description to $vmstatus_return_properties
    * reorder to run earlier before the cpu collection. Otherwise it might
      be skipped on the first call or when using `pvesh` if the cpu
      collection triggers 'next'.
    RFC:
    * collect memory info for all processes in cgroup directly without too
      generic helper function

 src/PVE/QemuServer.pm | 26 +++++++++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)

diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 59b13709..66cb9fd4 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -2433,6 +2433,12 @@ our $vmstatus_return_properties = {
         optional => 1,
         renderer => 'bytes',
     },
+    memhost => {
+        description => "Current memory usage on the host.",
+        type => 'integer',
+        optional => 1,
+        renderer => 'bytes',
+    },
     maxdisk => {
         description => "Root disk size in bytes.",
         type => 'integer',
@@ -2623,6 +2629,7 @@ sub vmstatus {
         $d->{uptime} = 0;
         $d->{cpu} = 0;
         $d->{mem} = 0;
+        $d->{memhost} = 0;
 
         $d->{netout} = 0;
         $d->{netin} = 0;
@@ -2675,6 +2682,24 @@ sub vmstatus {
             $d->{mem} = int(($pstat->{rss} / $pstat->{vsize}) * $d->{maxmem});
         }
 
+        my $fh = IO::File->new("/sys/fs/cgroup/qemu.slice/${vmid}.scope/cgroup.procs", "r");
+        if ($fh) {
+            while (my $childPid = <$fh>) {
+                chomp($childPid);
+                open(my $SMAPS_FH, '<', "/proc/$childPid/smaps_rollup")
+                    or die "failed to open PSS memory-stat from process - $!\n";
+
+                while (my $line = <$SMAPS_FH>) {
+                    if ($line =~ m/^Pss:\s+([0-9]+) kB$/) {
+                        $d->{memhost} = $d->{memhost} + int($1) * 1024;
+                        last;
+                    }
+                }
+                close $SMAPS_FH;
+            }
+        }
+        close($fh);
+
         my $pressures = PVE::ProcFSTools::read_cgroup_pressure("qemu.slice/${vmid}.scope");
         $d->{pressurecpusome} = $pressures->{cpu}->{some}->{avg10} * 1;
         $d->{pressurecpufull} = $pressures->{cpu}->{full}->{avg10} * 1;
@@ -2707,7 +2732,6 @@ sub vmstatus {
         } else {
             $d->{cpu} = $old->{cpu};
         }
-
     }
 
     return $res if !$full;
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH qemu-server v4 3/4] vmstatus: switch mem stat to PSS of VM cgroup
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (26 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 2/4] vmstatus: add memhost for host view of vm mem consumption Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 4/4] rrddata: use new pve-vm-9.0 rrd location if file is present Aaron Lauterer
                   ` (5 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

Instead of RSS, let's use the same PSS values as for the specific host
view as default, in case this value is not overwritten by the balloon
info.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    changes since:
    v2:
    * follow reorder of memhost collection, before cpu collection that might
      be trigger the next iteration of the loop in some situations

 src/PVE/QemuServer.pm | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 66cb9fd4..cdeaafa0 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -2678,10 +2678,6 @@ sub vmstatus {
 
         $d->{uptime} = int(($uptime - $pstat->{starttime}) / $cpuinfo->{user_hz});
 
-        if ($pstat->{vsize}) {
-            $d->{mem} = int(($pstat->{rss} / $pstat->{vsize}) * $d->{maxmem});
-        }
-
         my $fh = IO::File->new("/sys/fs/cgroup/qemu.slice/${vmid}.scope/cgroup.procs", "r");
         if ($fh) {
             while (my $childPid = <$fh>) {
@@ -2700,6 +2696,8 @@ sub vmstatus {
         }
         close($fh);
 
+        $d->{mem} = $d->{memhost};
+
         my $pressures = PVE::ProcFSTools::read_cgroup_pressure("qemu.slice/${vmid}.scope");
         $d->{pressurecpusome} = $pressures->{cpu}->{some}->{avg10} * 1;
         $d->{pressurecpufull} = $pressures->{cpu}->{full}->{avg10} * 1;
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH qemu-server v4 4/4] rrddata: use new pve-vm-9.0 rrd location if file is present
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (27 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 3/4] vmstatus: switch mem stat to PSS of VM cgroup Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH container v4 1/2] metrics: add pressures to metrics Aaron Lauterer
                   ` (4 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    changes since:
    RFC:
    * switch from pve9-vm to pve-vm-90 schema

 src/PVE/API2/Qemu.pm | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 09d4411b..105dd69e 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -1629,9 +1629,9 @@ __PACKAGE__->register_method({
     code => sub {
         my ($param) = @_;
 
-        return PVE::RRD::create_rrd_graph(
-            "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{ds}, $param->{cf},
-        );
+        my $path = "pve-vm-9.0/$param->{vmid}";
+        $path = "pve2-vm/$param->{vmid}" if !-e "/var/lib/rrdcached/db/${path}";
+        return PVE::RRD::create_rrd_graph($path, $param->{timeframe}, $param->{cf});
 
     },
 });
@@ -1673,8 +1673,9 @@ __PACKAGE__->register_method({
     code => sub {
         my ($param) = @_;
 
-        return PVE::RRD::create_rrd_data("pve2-vm/$param->{vmid}", $param->{timeframe},
-            $param->{cf});
+        my $path = "pve-vm-9.0/$param->{vmid}";
+        $path = "pve2-vm/$param->{vmid}" if !-e "/var/lib/rrdcached/db/${path}";
+        return PVE::RRD::create_rrd_data($path, $param->{timeframe}, $param->{cf});
     },
 });
 
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH container v4 1/2] metrics: add pressures to metrics
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (28 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 4/4] rrddata: use new pve-vm-9.0 rrd location if file is present Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-26  1:06 ` [pve-devel] [PATCH container v4 2/2] rrddata: use new pve-vm-9.0 rrd location if file is present Aaron Lauterer
                   ` (3 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

From: Folke Gleumes <f.gleumes@proxmox.com>

Originally-by: Folke Gleumes <f.gleumes@proxmox.com>
[AL:
    * rebased on current master
    * switch to new, more generic read_cgroup_pressure function
    * add pressures to return properties
]
Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    changes since:
    v2:
    * add return properties for pressures
    * reorder to run before cpu info collection, otherwise that might
      trigger 'next', skipping the pressure collection. For example when
      using `pvesh` for the 'current' API endpoint

 src/PVE/LXC.pm | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 741bb33..1870b4b 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -227,6 +227,32 @@ our $vmstatus_return_properties = {
         optional => 1,
         default => 0,
     },
+    pressurecpusome => {
+        description => "CPU Some pressure average over the last 10 seconds.",
+        type => 'number',
+        optional => 1,
+    },
+    pressureiosome => {
+        description => "IO Some pressure average over the last 10 seconds.",
+        type => 'number',
+        optional => 1,
+    },
+    pressureiofull => {
+        description => "IO Full pressure average over the last 10 seconds.",
+        type => 'number',
+        optional => 1,
+    },
+    pressurememorysome => {
+        description => "Memory Some pressure average over the last 10 seconds.",
+        type => 'number',
+        optional => 1,
+    },
+    pressurememoryfull => {
+        description => "Memory Full pressure average over the last 10 seconds.",
+        type => 'number',
+        optional => 1,
+    },
+
 };
 
 sub vmstatus {
@@ -329,6 +355,14 @@ sub vmstatus {
             $d->{diskwrite} = 0;
         }
 
+        my $pressures = PVE::ProcFSTools::read_cgroup_pressure("lxc/${vmid}");
+        $d->{pressurecpusome} = $pressures->{cpu}{some}{avg10};
+        $d->{pressurecpufull} = $pressures->{cpu}{full}{avg10};
+        $d->{pressureiosome} = $pressures->{io}{some}{avg10};
+        $d->{pressureiofull} = $pressures->{io}{full}{avg10};
+        $d->{pressurememorysome} = $pressures->{memory}{some}{avg10};
+        $d->{pressurememoryfull} = $pressures->{memory}{full}{avg10};
+
         if (defined(my $cpu = $cgroups->get_cpu_stat())) {
             # Total time (in milliseconds) used up by the cpu.
             my $used_ms = $cpu->{utime} + $cpu->{stime};
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] [PATCH container v4 2/2] rrddata: use new pve-vm-9.0 rrd location if file is present
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (29 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH container v4 1/2] metrics: add pressures to metrics Aaron Lauterer
@ 2025-07-26  1:06 ` Aaron Lauterer
  2025-07-28 14:42 ` [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Thomas Lamprecht
                   ` (2 subsequent siblings)
  33 siblings, 0 replies; 50+ messages in thread
From: Aaron Lauterer @ 2025-07-26  1:06 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---

Notes:
    changes since RFC:
    * switch from pve9-vm to pve-vm-90 schema

 src/PVE/API2/LXC.pm | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index a56c441..eb8873e 100644
--- a/src/PVE/API2/LXC.pm
+++ b/src/PVE/API2/LXC.pm
@@ -712,9 +712,9 @@ __PACKAGE__->register_method({
     code => sub {
         my ($param) = @_;
 
-        return PVE::RRD::create_rrd_graph(
-            "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{ds}, $param->{cf},
-        );
+        my $path = "pve-vm-9.0/$param->{vmid}";
+        $path = "pve2-vm/$param->{vmid}" if !-e "/var/lib/rrdcached/db/${path}";
+        return PVE::RRD::create_rrd_graph($path, $param->{timeframe}, $param->{cf});
 
     },
 });
@@ -756,8 +756,9 @@ __PACKAGE__->register_method({
     code => sub {
         my ($param) = @_;
 
-        return PVE::RRD::create_rrd_data("pve2-vm/$param->{vmid}", $param->{timeframe},
-            $param->{cf});
+        my $path = "pve-vm-9.0/$param->{vmid}";
+        $path = "pve2-vm/$param->{vmid}" if !-e "/var/lib/rrdcached/db/${path}";
+        return PVE::RRD::create_rrd_data($path, $param->{timeframe}, $param->{cf});
     },
 });
 
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH proxmox-rrd-migration-tool v4 1/3] create proxmox-rrd-migration-tool
  2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 1/3] create proxmox-rrd-migration-tool Aaron Lauterer
@ 2025-07-28 14:25   ` Lukas Wagner
  0 siblings, 0 replies; 50+ messages in thread
From: Lukas Wagner @ 2025-07-28 14:25 UTC (permalink / raw)
  To: Proxmox VE development discussion; +Cc: pve-devel

Hey Aaron,

some comments inline.


On Sat Jul 26, 2025 at 3:05 AM CEST, Aaron Lauterer wrote:
> This tool is intended to migrate the Proxmox VE (PVE) RRD data files to
> the new schema.
>
> Up until PVE8 the schema has been the same for a long time. With PVE9 we
> introduced new columns to guests (vm) and nodes. We also switched all
> types (vm, node, storate) to the same aggregation schemas as we do it in
> PBS.
> The result of both are a much finer resolution for long time spans, but
> also larger RRD files.
>
> * node: 79K -> 1.4M
> * vm: 66K -> 1.3m
> * storage: 14K -> 156K
>
> The old directories for VMs used to be in `/var/lib/rrdcached/db/` with
> the following sub directories:
>
> * nodes: `pve2-node`
> * guests (VM/CT): `pve2-vm`
> * storage: `pve2-storage`
>
> With this change we also introduce a new key schema, that makes it
> easier in the future to introduce new ones. Instead of the
> `pve{version}-{type}` we are switching to `pve-{type}-{version}`.
>
> This enables us to add new columns with a new version, without breaking
> nodes that are not yet updated. We are NOT allowed to remove or re-use
> existing columns. That would be a breaking change.
> We are currently at version 9.0. But in the future, if needed, this tool
> can be adapted to do other migrations too.
> For example, {old, 9.0} -> 9.2, should that be necessary.
>
> The actual migration is handled by `librrd` to which we pass the path to
> the old and new files, and the new RRD definitions. The `rrd_create_r2`
> call then does the hard work of migrating and converting exisiting data
> into the new file and aggregation schema.
>
> This can take some time. Quick tests on a Ryzen 7900X with the following
> files:
> * 1 node RRD file
> * 10k vm RRD files
> * 1 storage RRD file
>
> showed the folling results:
>
> * 1 thread:  179.61s user 14.82s system 100% cpu 3:14.17 total
> * 4 threads: 187.57s user 16.98s system 399% cpu 51.198 total
>
> This is why we do not migrate inline, but have it as a separate step
> during package upgrades.
>
> Behavior: By default nothing will be changed and a dry or test run will
> happen.
> Only if the `--migrate` parameter is added will the actual migration be
> done.
>
> For each found RRD source file, the tool checks if a matching target
> file already exists. By default, those will be skipped to not overwrite
> target files that might already store newer data.
> With the `--force` parameter this can be changed.
>
> That means, one can run the tool multiple times (without --force) and it
> will pick up where it might have left off. For example it the migration
> was interrupted for some reason.
>
> Once a source file has been processed it will be renamed with the `.old`
> appendix. It will be excluded from future runs as we check for files
> without an extension.
>
> The tool has some simple heuristic to determine how many threads should
> be used. Be default the range is between 1 to 4 threads. But the
> `--threads` parameter allows a manual override.
>
> Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
> ---
>  .cargo/config.toml      |   5 +
>  .gitignore              |   9 +
>  Cargo.toml              |  20 ++
>  build.rs                |  29 ++
>  src/lib.rs              |   5 +
>  src/main.rs             | 567 ++++++++++++++++++++++++++++++++++++++++
>  src/parallel_handler.rs | 160 ++++++++++++
>  wrapper.h               |   1 +
>  8 files changed, 796 insertions(+)
>  create mode 100644 .cargo/config.toml
>  create mode 100644 .gitignore
>  create mode 100644 Cargo.toml
>  create mode 100644 build.rs
>  create mode 100644 src/lib.rs
>  create mode 100644 src/main.rs
>  create mode 100644 src/parallel_handler.rs
>  create mode 100644 wrapper.h
>
> diff --git a/.cargo/config.toml b/.cargo/config.toml
> new file mode 100644
> index 0000000..3b5b6e4
> --- /dev/null
> +++ b/.cargo/config.toml
> @@ -0,0 +1,5 @@
> +[source]
> +[source.debian-packages]
> +directory = "/usr/share/cargo/registry"
> +[source.crates-io]
> +replace-with = "debian-packages"
> diff --git a/.gitignore b/.gitignore
> new file mode 100644
> index 0000000..06ac1a1
> --- /dev/null
> +++ b/.gitignore
> @@ -0,0 +1,9 @@
> +*.build
> +*.buildinfo
> +*.changes
> +*.deb
> +*.dsc
> +*.tar*
> +target/
> +/Cargo.lock
> +/proxmox-rrd-migration-tool-[0-9]*/
> diff --git a/Cargo.toml b/Cargo.toml
> new file mode 100644
> index 0000000..d3523f3
> --- /dev/null
> +++ b/Cargo.toml
> @@ -0,0 +1,20 @@
> +[package]
> +name = "proxmox_rrd_migration_8-9"
> +version = "0.1.0"
> +edition = "2021"
> +authors = [
> +    "Aaron Lauterer <a.lauterer@proxmox.com>",
> +    "Proxmox Support Team <support@proxmox.com>",
> +]
> +license = "AGPL-3"
> +homepage = "https://www.proxmox.com"
> +
> +[dependencies]
> +anyhow = "1.0.86"
> +pico-args = "0.5.0"
> +proxmox-async = "0.4"
> +crossbeam-channel = "0.5"
> +
> +[build-dependencies]
> +bindgen = "0.66.1"
> +pkg-config = "0.3"
> diff --git a/build.rs b/build.rs
> new file mode 100644
> index 0000000..56d07cc
> --- /dev/null
> +++ b/build.rs
> @@ -0,0 +1,29 @@
> +use std::env;
> +use std::path::PathBuf;
> +
> +fn main() {
> +    println!("cargo:rustc-link-lib=rrd");
> +
> +    println!("cargo:rerun-if-changed=wrapper.h");
> +    // The bindgen::Builder is the main entry point
> +    // to bindgen, and lets you build up options for
> +    // the resulting bindings.
> +
> +    let bindings = bindgen::Builder::default()
> +        // The input header we would like to generate
> +        // bindings for.
> +        .header("wrapper.h")
> +        // Tell cargo to invalidate the built crate whenever any of the
> +        // included header files changed.
> +        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
> +        // Finish the builder and generate the bindings.
> +        .generate()
> +        // Unwrap the Result and panic on failure.
> +        .expect("Unable to generate bindings");
> +
> +    // Write the bindings to the $OUT_DIR/bindings.rs file.
> +    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
> +    bindings
> +        .write_to_file(out_path.join("bindings.rs"))
> +        .expect("Couldn't write bindings!");
> +}
> diff --git a/src/lib.rs b/src/lib.rs
> new file mode 100644
> index 0000000..a38a13a
> --- /dev/null
> +++ b/src/lib.rs
> @@ -0,0 +1,5 @@
> +#![allow(non_upper_case_globals)]
> +#![allow(non_camel_case_types)]
> +#![allow(non_snake_case)]
> +
> +include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
> diff --git a/src/main.rs b/src/main.rs
> new file mode 100644
> index 0000000..5e6418c
> --- /dev/null
> +++ b/src/main.rs
> @@ -0,0 +1,567 @@
> +use anyhow::{bail, Error, Result};
> +use std::{
> +    ffi::{CStr, CString, OsString},
> +    fs,
> +    os::unix::{ffi::OsStrExt, fs::PermissionsExt},
> +    path::{Path, PathBuf},
> +    sync::Arc,
> +};
> +
> +use proxmox_rrd_migration_tool::{rrd_clear_error, rrd_create_r2, rrd_get_context, rrd_get_error};
> +
> +use crate::parallel_handler::ParallelHandler;
> +
> +pub mod parallel_handler;
> +
> +const BASE_DIR: &str = "/var/lib/rrdcached/db";
> +const SOURCE_SUBDIR_NODE: &str = "pve2-node";
> +const SOURCE_SUBDIR_GUEST: &str = "pve2-vm";
> +const SOURCE_SUBDIR_STORAGE: &str = "pve2-storage";
> +const TARGET_SUBDIR_NODE: &str = "pve-node-9.0";
> +const TARGET_SUBDIR_GUEST: &str = "pve-vm-9.0";
> +const TARGET_SUBDIR_STORAGE: &str = "pve-storage-9.0";
> +const RESOURCE_BASE_DIR: &str = "/etc/pve";
> +const MAX_THREADS: usize = 4;
> +const RRD_STEP_SIZE: usize = 60;
> +
> +type File = (CString, OsString);

Maybe use some different name here in order to avoid confusion with
std::fs::File? e.g. RRDFile

> +
> +// RRAs are defined in the following way:
> +//
> +// RRA:CF:xff:step:rows
> +// CF: AVERAGE or MAX
> +// xff: 0.5
> +// steps: stepsize is defined on rrd file creation! example: with 60 seconds step size:
> +//	e.g. 1 => 60 sec, 30 => 1800 seconds or 30 min
> +// rows: how many aggregated rows are kept, as in how far back in time we store data
> +//
> +// how many seconds are aggregated per RRA: steps * stepsize * rows
> +// how many hours are aggregated per RRA: steps * stepsize * rows / 3600
> +// how many days are aggregated per RRA: steps * stepsize * rows / 3600 / 24
> +// https://oss.oetiker.ch/rrdtool/tut/rrd-beginners.en.html#Understanding_by_an_example
> +
> +const RRD_VM_DEF: [&CStr; 25] = [
> +    c"DS:maxcpu:GAUGE:120:0:U",
> +    c"DS:cpu:GAUGE:120:0:U",
> +    c"DS:maxmem:GAUGE:120:0:U",
> +    c"DS:mem:GAUGE:120:0:U",
> +    c"DS:maxdisk:GAUGE:120:0:U",
> +    c"DS:disk:GAUGE:120:0:U",
> +    c"DS:netin:DERIVE:120:0:U",
> +    c"DS:netout:DERIVE:120:0:U",
> +    c"DS:diskread:DERIVE:120:0:U",
> +    c"DS:diskwrite:DERIVE:120:0:U",
> +    c"DS:memhost:GAUGE:120:0:U",
> +    c"DS:pressurecpusome:GAUGE:120:0:U",
> +    c"DS:pressurecpufull:GAUGE:120:0:U",
> +    c"DS:pressureiosome:GAUGE:120:0:U",
> +    c"DS:pressureiofull:GAUGE:120:0:U",
> +    c"DS:pressurememorysome:GAUGE:120:0:U",
> +    c"DS:pressurememoryfull:GAUGE:120:0:U",
> +    c"RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
> +    c"RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
> +    c"RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
> +    c"RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
> +    c"RRA:MAX:0.5:1:1440",        // 1 min * 1440 => 1 day
> +    c"RRA:MAX:0.5:30:1440",       // 30 min * 1440 => 30 day
> +    c"RRA:MAX:0.5:360:1440",      // 6 hours * 1440 => 360 day ~1 year
> +    c"RRA:MAX:0.5:10080:570",     // 1 week * 570 => ~10 years
> +];
> +
> +const RRD_NODE_DEF: [&CStr; 27] = [
> +    c"DS:loadavg:GAUGE:120:0:U",
> +    c"DS:maxcpu:GAUGE:120:0:U",
> +    c"DS:cpu:GAUGE:120:0:U",
> +    c"DS:iowait:GAUGE:120:0:U",
> +    c"DS:memtotal:GAUGE:120:0:U",
> +    c"DS:memused:GAUGE:120:0:U",
> +    c"DS:swaptotal:GAUGE:120:0:U",
> +    c"DS:swapused:GAUGE:120:0:U",
> +    c"DS:roottotal:GAUGE:120:0:U",
> +    c"DS:rootused:GAUGE:120:0:U",
> +    c"DS:netin:DERIVE:120:0:U",
> +    c"DS:netout:DERIVE:120:0:U",
> +    c"DS:memfree:GAUGE:120:0:U",
> +    c"DS:arcsize:GAUGE:120:0:U",
> +    c"DS:pressurecpusome:GAUGE:120:0:U",
> +    c"DS:pressureiosome:GAUGE:120:0:U",
> +    c"DS:pressureiofull:GAUGE:120:0:U",
> +    c"DS:pressurememorysome:GAUGE:120:0:U",
> +    c"DS:pressurememoryfull:GAUGE:120:0:U",
> +    c"RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
> +    c"RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
> +    c"RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
> +    c"RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
> +    c"RRA:MAX:0.5:1:1440",        // 1 min * 1440 => 1 day
> +    c"RRA:MAX:0.5:30:1440",       // 30 min * 1440 => 30 day
> +    c"RRA:MAX:0.5:360:1440",      // 6 hours * 1440 => 360 day ~1 year
> +    c"RRA:MAX:0.5:10080:570",     // 1 week * 570 => ~10 years
> +];
> +
> +const RRD_STORAGE_DEF: [&CStr; 10] = [
> +    c"DS:total:GAUGE:120:0:U",
> +    c"DS:used:GAUGE:120:0:U",
> +    c"RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
> +    c"RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
> +    c"RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
> +    c"RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
> +    c"RRA:MAX:0.5:1:1440",        // 1 min * 1440 => 1 day
> +    c"RRA:MAX:0.5:30:1440",       // 30 min * 1440 => 30 day
> +    c"RRA:MAX:0.5:360:1440",      // 6 hours * 1440 => 360 day ~1 year
> +    c"RRA:MAX:0.5:10080:570",     // 1 week * 570 => ~10 years
> +];
> +
> +const HELP: &str = "\
> +proxmox-rrd-migration tool
> +
> +Migrates existing RRD graph data to the new format.
> +
> +Use this only in the process of upgrading from Proxmox VE 8 to 9 according to the upgrade guide!
> +
> +USAGE:
> +    proxmox-rrd-migration [OPTIONS]
> +
> +    FLAGS:
> +        -h, --help              Prints this help information
> +
> +    OPTIONS:
> +        --migrate               Start the migration. Without it, only a dry run will be done.
> +
> +        --force                 Migrate, even if the target already exists.
> +                                This will overwrite any migrated RRD files!
> +
> +        --threads THREADS       Number of paralell threads.
> +
> +        --source <SOURCE DIR>   Source base directory. Mainly for tests!
> +                                Default: /var/lib/rrdcached/db
> +
> +        --target <TARGET DIR>   Target base directory. Mainly for tests!
> +                                Default: /var/lib/rrdcached/db
> +
> +        --resources <DIR>       Directory that contains .vmlist and .member files. Mainly for tests!
> +                                Default: /etc/pve
> +
> +";
> +
> +#[derive(Debug)]
> +struct Args {
> +    migrate: bool,
> +    force: bool,
> +    threads: Option<usize>,
> +    source: Option<String>,
> +    target: Option<String>,
> +    resources: Option<String>,
> +}
> +
> +fn parse_args() -> Result<Args, Error> {
> +    let mut pargs = pico_args::Arguments::from_env();
> +
> +    // Help has a higher priority and should be handled separately.
> +    if pargs.contains(["-h", "--help"]) {
> +        print!("{}", HELP);
> +        std::process::exit(0);
> +    }
> +
> +    let mut args = Args {
> +        migrate: false,
> +        threads: pargs
> +            .opt_value_from_str("--threads")
> +            .expect("Could not parse --threads parameter"),
> +        force: false,
> +        source: pargs
> +            .opt_value_from_str("--source")
> +            .expect("Could not parse --source parameter"),
> +        target: pargs
> +            .opt_value_from_str("--target")
> +            .expect("Could not parse --target parameter"),
> +        resources: pargs
> +            .opt_value_from_str("--resources")
> +            .expect("Could not parse --resources parameter"),
> +    };
> +
> +    if pargs.contains("--migrate") {
> +        args.migrate = true;
> +    }
> +    if pargs.contains("--force") {
> +        args.force = true;
> +    }
> +
> +    // It's up to the caller what to do with the remaining arguments.
> +    let remaining = pargs.finish();
> +    if !remaining.is_empty() {
> +        bail!(format!("Warning: unused arguments left: {:?}", remaining));

No need to use format! here, bail! supports formatting natively:

	bail!("Warning: .... {remaining:?}");

> +    }
> +
> +    Ok(args)
> +}
> +
> +fn main() {
> +    let args = match parse_args() {
> +        Ok(v) => v,
> +        Err(e) => {
> +            eprintln!("Error: {}.", e);
> +            std::process::exit(1);
> +        }
> +    };
> +
> +    let source_base_dir = match args.source {
> +        Some(ref v) => v.as_str(),
> +        None => BASE_DIR,
> +    };

you can use this instead, it's shorter and a bit nicer to read IMO:
       let source_base_dir = args.source.as_deref().unwrap_or(BASE_DIR);

> +
> +    let target_base_dir = match args.target {
> +        Some(ref v) => v.as_str(),
> +        None => BASE_DIR,
> +    };
> +
> +    let resource_base_dir = match args.resources {
> +        Some(ref v) => v.as_str(),
> +        None => RESOURCE_BASE_DIR,
> +    };

same for the previous two

> +
> +    let source_dir_guests: PathBuf = [source_base_dir, SOURCE_SUBDIR_GUEST].iter().collect();
> +    let target_dir_guests: PathBuf = [target_base_dir, TARGET_SUBDIR_GUEST].iter().collect();
> +    let source_dir_nodes: PathBuf = [source_base_dir, SOURCE_SUBDIR_NODE].iter().collect();
> +    let target_dir_nodes: PathBuf = [target_base_dir, TARGET_SUBDIR_NODE].iter().collect();
> +    let source_dir_storage: PathBuf = [source_base_dir, SOURCE_SUBDIR_STORAGE].iter().collect();
> +    let target_dir_storage: PathBuf = [target_base_dir, TARGET_SUBDIR_STORAGE].iter().collect();


What do you think about:

let source_base_dir = Path::new(args.source.as_deref().unwrap_or(BASE_DIR));
let target_base_dir = Path::new(args.target.as_deref().unwrap_or(BASE_DIR));

let source_dir_guests = source_base_dir.join(SOURCE_SUBDIR_GUEST);
let target_dir_guests = source_base_dir.join(SOURCE_SUBDIR_GUEST);



> +
> +    if !args.migrate {
> +        println!("DRYRUN! Use the --migrate parameter to start the migration.");
> +    }
> +    if args.force {
> +        println!("Force mode! Will overwrite existing target RRD files!");
> +    }
> +
> +    if let Err(e) = migrate_nodes(
> +        source_dir_nodes,
> +        target_dir_nodes,
> +        resource_base_dir,
> +        args.migrate,
> +        args.force,
> +    ) {
> +        eprintln!("Error migrating nodes: {}", e);
> +        std::process::exit(1);
> +    }
> +    if let Err(e) = migrate_storage(
> +        source_dir_storage,
> +        target_dir_storage,
> +        args.migrate,
> +        args.force,
> +    ) {
> +        eprintln!("Error migrating storage: {}", e);
> +        std::process::exit(1);
> +    }
> +    if let Err(e) = migrate_guests(
> +        source_dir_guests,
> +        target_dir_guests,
> +        resource_base_dir,
> +        set_threads(&args),
> +        args.migrate,
> +        args.force,
> +    ) {
> +        eprintln!("Error migrating guests: {}", e);
> +        std::process::exit(1);
> +    }

Error handling in this function could be a bit cleaner if broken out
into a separate function and by using anyhow's .context/.with_context:

fn do_main() -> Result<(), Error> {
    let args = parse_args(...).context("Could not parse args")?;
    ...
    migrate_guests(...).context("Error migrating guests")?;

    Ok(())
}

fn main() {
    if let Err(e) = do_main() {
        eprintln!("{e}");
	std::process:exit(1);
    }
}

What do you think?


> +}
> +
> +/// Set number of threads
> +///
> +/// Either a fixed parameter or determining a range between 1 to 4 threads
> +///  based on the number of CPU cores available in the system.
> +fn set_threads(args: &Args) -> usize {

I think the name 'set_threads' is rather confusing for something that
*returns* the number of threads to use. Maybe call it
'threads_from_core_count' or something alike? (under the assumption that
you remove the let Some(...) as suggested below. If you keep it there,
'get_threads' might be an ok choice.



> +    if let Some(threads) = args.threads {
> +        return threads;
> +    }

^ Personally I'd keep this part outside of the helper, but no hard
feelings.

fn do_main() {
    ...

    let threads = args.threads.unwrap_or_else(threads_from_core_count);
    migrate_guests(..., threads)?;

    Ok(())
}


fn threads_from_core_count() -> usize {
  ...
}



> +
> +    // check for a way to get physical cores and not threads?
> +    let cpus: usize = String::from_utf8_lossy(
> +        std::process::Command::new("nproc")
> +            .output()
> +            .expect("Error running nproc")
> +            .stdout
> +            .as_slice()
> +            .trim_ascii(),
> +    )
> +    .parse::<usize>()
> +    .expect("Could not parse nproc output");
> +
> +    if cpus < 32 {
> +        let threads = cpus / 8;
> +        if threads == 0 {
> +            return 1;
> +        }
> +        return threads;
> +    }
> +    MAX_THREADS
> +}
> +
> +/// Check if a VMID is currently configured
> +fn resource_present(path: &str, resource: &str) -> Result<bool> {
> +    let resourcelist = fs::read_to_string(path)?;
> +    Ok(resourcelist.contains(format!("\"{resource}\"").as_str()))
> +}
> +
> +/// Rename file to old, when migrated or resource not present at all -> old RRD file
> +fn mv_old(file: &str) -> Result<()> {
> +    let old = format!("{}.old", file);
> +    fs::rename(file, old)?;
> +    Ok(())
> +}
> +
> +/// Colllect all RRD files in the provided directory
> +fn collect_rrd_files(location: &PathBuf) -> Result<Vec<(CString, OsString)>> {
                                                             ^
Maybe use the type you've defined here? `File`, although I'd 
prefer a different name to avoid confusion with std::fs::File.

> +    let mut files: Vec<(CString, OsString)> = Vec::new();
> +
> +    fs::read_dir(location)?
> +        .filter(|f| f.is_ok())
> +        .map(|f| f.unwrap().path())

You can use filter_map here, maybe like this:

fs::read_dir(location)?
    .filter_map(|f| match f {
        Ok(a) => Some(a.path()),
        Err(e) => {
            eprintln!("could not read dir entry: {e}");
            None
        }
    })

or, if you don't want to log the error:

fs::read_dir(location)?
    .filter_map(Result::ok)
    .map(|entry| entry.path())

(untested, but you get the idea)

> +        .filter(|f| f.is_file() && f.extension().is_none())
> +        .for_each(|file| {
> +            let path = CString::new(file.as_path().as_os_str().as_bytes())
> +                .expect("Could not convert path to CString.");
                      ^

> +            let fname = file
> +                .file_name()
> +                .map(|v| v.to_os_string())

Reading the docs for the CString::new function, it should only fail if
there is a NUL byte in the string, which should AFAIK be impossible
here since the string came from the file name. Maybe express that in
some comment here?
                       v

> +                .expect("Could not convert fname to OsString.");
> +            files.push((path, fname))
> +        });
> +    Ok(files)
> +}
> +
> +/// Does the actual migration for the given file
> +fn do_rrd_migration(
> +    file: File,
> +    target_location: &Path,
> +    rrd_def: &[&CStr],
> +    migrate: bool,
> +    force: bool,
> +) -> Result<()> {
> +    if !migrate {
> +        println!("would migrate but in dry run mode");
> +    }
> +
> +    let resource = file.1;
> +    let mut target_path = target_location.to_path_buf();

Since the first thing you do with target_location is to convert it to
a PathBuf, I'd suggest just passing it as a PathBuf and let the caller
take care of the allocation.

> +    target_path.push(resource);
> +
> +    if target_path.exists() && !force {
> +        println!(
> +            "already migrated, use --force to overwrite target file: {}",
> +            target_path.display()
> +        );
> +    }
> +
> +    if !migrate || (target_path.exists() && !force) {
> +        bail!("skipping");
> +    }

you could pull out the 'target_path.exists() && !force' into a variable so
that you don't have to evaluate the same thing twice

> +
> +    let mut source: [*const i8; 2] = [std::ptr::null(); 2];
> +    source[0] = file.0.as_ptr();
> +
> +    let target_path = CString::new(target_path.to_str().unwrap()).unwrap();
> +
> +    unsafe {
> +        rrd_get_context();
> +        rrd_clear_error();
> +        let res = rrd_create_r2(
> +            target_path.as_ptr(),
> +            RRD_STEP_SIZE as u64,
> +            0,
> +            0,
> +            source.as_mut_ptr(),
> +            std::ptr::null(),
> +            rrd_def.len() as i32,
> +            rrd_def
> +                .iter()
> +                .map(|v| v.as_ptr())
> +                .collect::<Vec<_>>()
> +                .as_mut_ptr(),
> +        );
> +        if res != 0 {
> +            bail!(
> +                "RRD create Error: {}",
> +                CStr::from_ptr(rrd_get_error()).to_string_lossy()
> +            );
> +        }
> +    }
> +    Ok(())
> +}
> +
> +/// Migrate guest RRD files
> +///
> +/// In parallel to speed up the process as most time is spent on converting the
> +/// data to the new format.
> +fn migrate_guests(
> +    source_dir_guests: PathBuf,
> +    target_dir_guests: PathBuf,
> +    resources: &str,
> +    threads: usize,
> +    migrate: bool,
> +    force: bool,
> +) -> Result<(), Error> {
> +    println!("Migrating RRD data for guests…");
> +    println!("Using {} thread(s)", threads);
> +
> +    let guest_source_files = collect_rrd_files(&source_dir_guests)?;
> +
> +    if !target_dir_guests.exists() && migrate {
> +        println!("Creating new directory: '{}'", target_dir_guests.display());
> +        std::fs::create_dir(&target_dir_guests)?;
> +    }
> +
> +    let total_guests = guest_source_files.len();
> +    let guests = Arc::new(std::sync::atomic::AtomicUsize::new(0));
> +    let guests2 = guests.clone();

Just FIY, when cloning an Arc it's better to use Arc::clone(&guests),
because this makes it clearer *what* you are a actually cloning,
the Arc vs. the content of the Arc

> +    let start_time = std::time::SystemTime::now();

Since you only measure the elapsed time it might be more idiomatic to
use std::time::Instant here, but not hard feelings.

> +
> +    let migration_pool = ParallelHandler::new(
> +        "guest rrd migration",
> +        threads,
> +        move |file: (CString, OsString)| {

Please add some comment regarding the .unwrap here.

> +            let full_path = file.0.clone().into_string().unwrap();
> +
> +            if let Ok(()) = do_rrd_migration(
> +                file,
> +                &target_dir_guests,
> +                RRD_VM_DEF.as_slice(),
> +                migrate,
> +                force,
> +            ) {


Since do_rrd_migration does not return any data in the Option, you could
just 

if do_rrd_migration(....).is_ok() {
   ....
}

> +                mv_old(full_path.as_str())?;
> +                let current_guests = guests2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
> +                if current_guests > 0 && current_guests % 200 == 0 {
> +                    println!("Migrated {} of {} guests", current_guests, total_guests);
> +                }
> +            }
> +            Ok(())
> +        },
> +    );
> +    let migration_channel = migration_pool.channel();
> +
> +    for file in guest_source_files {
> +        let node = file.1.clone().into_string().unwrap();
> +        if !resource_present(format!("{resources}/.vmlist").as_str(), node.as_str())? {
> +            println!("VMID: '{node}' not present. Skip and mark as old.");
> +            mv_old(format!("{}", file.0.to_string_lossy()).as_str())?;
> +        }
> +        let migration_channel = migration_channel.clone();

Is this clone here needed? Seems to compile fine without here....

> +        migration_channel.send(file)?;
> +    }
> +
> +    drop(migration_channel);
> +    migration_pool.complete()?;
> +
> +    let elapsed = start_time.elapsed()?.as_secs_f64();
> +    let guests = guests.load(std::sync::atomic::Ordering::SeqCst);
> +    println!("Migrated {} guests", guests);
> +    println!("It took {:.2}s", elapsed);
> +
> +    Ok(())
> +}
> +
> +/// Migrate node RRD files
> +///
> +/// In serial as the number of nodes will not be high.
> +fn migrate_nodes(
> +    source_dir_nodes: PathBuf,
> +    target_dir_nodes: PathBuf,
> +    resources: &str,

Any reason why this one is a &str instead of a PathBuf? As far as I can
tell it is also a path (/etc/pve by default). Also the
name of the variable somehow makes it not really clear that this is
suppose to be a path, I only deduced it from RESOURCE_BASE_DIR.

> +    migrate: bool,
> +    force: bool,
> +) -> Result<(), Error> {
> +    println!("Migrating RRD data for nodes…");
> +
> +    if !target_dir_nodes.exists() && migrate {
> +        println!("Creating new directory: '{}'", target_dir_nodes.display());
> +        std::fs::create_dir(&target_dir_nodes)?;
> +    }
> +
> +    let node_source_files = collect_rrd_files(&source_dir_nodes)?;
> +
> +    for file in node_source_files {
> +        let node = file.1.clone().into_string().unwrap();
> +        let full_path = file.0.clone().into_string().unwrap();

Please add some comment why it is okay to .unwrap here (or just return
or ignore the error, if that makes more sense).


> +        println!("Node: '{node}'");
> +        if !resource_present(format!("{resources}/.members").as_str(), node.as_str())? {

You can just use &format!... and &node instead of the .as_str() calls
(a bit nicer to read and more idionmatic, but no hard feelings).

> +            println!("Node: '{node}' not present. Skip and mark as old.");
> +            mv_old(format!("{}/{}", file.0.to_string_lossy(), node).as_str())?;
> +        }
> +        if let Ok(()) = do_rrd_migration(
> +            file,
> +            &target_dir_nodes,
> +            RRD_NODE_DEF.as_slice(),
> +            migrate,
> +            force,
> +        ) {


Since do_rrd_migration does not return any data in the Option, you could
just 

if do_rrd_migration(....).is_ok() {
   ....
}

> +            mv_old(full_path.as_str())?;
> +        }
> +    }
> +    println!("Migrated all nodes");
> +
> +    Ok(())
> +}
> +
> +/// Migrate storage RRD files
> +///
> +/// In serial as the number of storage will not be that high.
> +fn migrate_storage(
> +    source_dir_storage: PathBuf,
> +    target_dir_storage: PathBuf,
> +    migrate: bool,
> +    force: bool,
> +) -> Result<(), Error> {
> +    println!("Migrating RRD data for storages…");
> +
> +    if !target_dir_storage.exists() && migrate {
> +        println!("Creating new directory: '{}'", target_dir_storage.display());
> +        std::fs::create_dir(&target_dir_storage)?;
> +    }
> +
> +    // storage has another layer of directories per node over which we need to iterate
> +    fs::read_dir(&source_dir_storage)?
> +        .filter(|f| f.is_ok())
> +        .map(|f| f.unwrap().path())
> +        .filter(|f| f.is_dir())

you can use filter_map here, as explained in collect_rrd_files

> +        .try_for_each(|node| {
> +            let mut source_storage_subdir = source_dir_storage.clone();
> +            source_storage_subdir.push(node.file_name().unwrap());
> +
> +            let mut target_storage_subdir = target_dir_storage.clone();
> +            target_storage_subdir.push(node.file_name().unwrap());
> +
> +            if !target_storage_subdir.exists() && migrate {
> +                fs::create_dir(target_storage_subdir.as_path())?;
You can use & here instead of .as_path() :)

> +                let metadata = target_storage_subdir.metadata()?;
> +                let mut permissions = metadata.permissions();
> +                permissions.set_mode(0o755);

You need to actually apply the permissions to the dir, here you only set
the permission bits in the Permissions data type.

std::fs::set_permissions(...)

> +            }
> +
> +            let storage_source_files = collect_rrd_files(&source_storage_subdir)?;
> +
> +            for file in storage_source_files {
> +                println!(
> +                    "Storage: '{}/{}'",
> +                    node.file_name()
> +                        .expect("no file name present")

Same thing here regarding the potential panic

> +                        .to_string_lossy(),
> +                    PathBuf::from(file.1.clone()).display()

Starting with rustc 1.87, you can directly call file.1.display() on the underlying OsStr(ing).

> +                );
> +
> +                let full_path = file.0.clone().into_string().unwrap();
> +                if let Ok(()) = do_rrd_migration(
> +                    file,
> +                    &target_storage_subdir,
> +                    RRD_STORAGE_DEF.as_slice(),
> +                    migrate,
> +                    force,
> +                ) {
> +                    mv_old(full_path.as_str())?;
> +                }

Since do_rrd_migration does not return any data in the Option, you could
just 

if do_rrd_migration(....).is_ok() {
   ....
}

> +            }
> +            Ok::<(), Error>(())
> +        })?;
> +    println!("Migrated all storages");
> +
> +    Ok(())
> +}
> diff --git a/src/parallel_handler.rs b/src/parallel_handler.rs
> new file mode 100644
> index 0000000..d8ee3c7
> --- /dev/null
> +++ b/src/parallel_handler.rs
> @@ -0,0 +1,160 @@
> +//! A thread pool which run a closure in parallel.
> +
> +use std::sync::{Arc, Mutex};
> +use std::thread::JoinHandle;
> +
> +use anyhow::{bail, format_err, Error};
> +use crossbeam_channel::{bounded, Sender};
> +
> +/// A handle to send data to the worker thread (implements clone)
> +pub struct SendHandle<I> {
> +    input: Sender<I>,
> +    abort: Arc<Mutex<Option<String>>>,
> +}
> +
> +/// Returns the first error happened, if any
> +pub fn check_abort(abort: &Mutex<Option<String>>) -> Result<(), Error> {
> +    let guard = abort.lock().unwrap();
> +    if let Some(err_msg) = &*guard {
> +        return Err(format_err!("{}", err_msg));
> +    }
> +    Ok(())
> +}
> +
> +impl<I: Send> SendHandle<I> {
> +    /// Send data to the worker threads
> +    pub fn send(&self, input: I) -> Result<(), Error> {
> +        check_abort(&self.abort)?;
> +        match self.input.send(input) {
> +            Ok(()) => Ok(()),
> +            Err(_) => bail!("send failed - channel closed"),
> +        }

might be more idiomatic to use .map_err here

> +    }
> +}
> +
> +/// A thread pool which run the supplied closure
> +///
> +/// The send command sends data to the worker threads. If one handler
> +/// returns an error, we mark the channel as failed and it is no
> +/// longer possible to send data.
> +///
> +/// When done, the 'complete()' method needs to be called to check for
> +/// outstanding errors.
> +pub struct ParallelHandler<I> {
> +    handles: Vec<JoinHandle<()>>,
> +    name: String,
> +    input: Option<SendHandle<I>>,
> +}
> +
> +impl<I> Clone for SendHandle<I> {
> +    fn clone(&self) -> Self {
> +        Self {
> +            input: self.input.clone(),
> +            abort: Arc::clone(&self.abort),
> +        }
> +    }
> +}
> +
> +impl<I: Send + 'static> ParallelHandler<I> {
> +    /// Create a new thread pool, each thread processing incoming data
> +    /// with 'handler_fn'.
> +    pub fn new<F>(name: &str, threads: usize, handler_fn: F) -> Self
> +    where
> +        F: Fn(I) -> Result<(), Error> + Send + Clone + 'static,
> +    {
> +        let mut handles = Vec::new();
> +        let (input_tx, input_rx) = bounded::<I>(threads);
> +
> +        let abort = Arc::new(Mutex::new(None));
> +
> +        for i in 0..threads {
> +            let input_rx = input_rx.clone();
> +            let abort = Arc::clone(&abort);
> +            let handler_fn = handler_fn.clone();
> +
> +            handles.push(
> +                std::thread::Builder::new()
> +                    .name(format!("{} ({})", name, i))
> +                    .spawn(move || loop {
> +                        let data = match input_rx.recv() {
> +                            Ok(data) => data,
> +                            Err(_) => return,
> +                        };
> +                        if let Err(err) = (handler_fn)(data) {
> +                            let mut guard = abort.lock().unwrap();

.unwrap on Mutex::lock is fine IMO, but should have a comment explaining
that it only .unwrap's on a poisioned mutex.

> +                            if guard.is_none() {
> +                                *guard = Some(err.to_string());
> +                            }
> +                        }
> +                    })
> +                    .unwrap(),

This shouldn't .unwrap() IMO, rather return an error from this function.

> +            );
> +        }
> +        Self {
> +            handles,
> +            name: name.to_string(),
> +            input: Some(SendHandle {
> +                input: input_tx,
> +                abort,
> +            }),
> +        }
> +    }
> +
> +    /// Returns a cloneable channel to send data to the worker threads
> +    pub fn channel(&self) -> SendHandle<I> {
> +        self.input.as_ref().unwrap().clone()

Please add a comment why .unwrap is okay here or bubble some error up

> +    }
> +
> +    /// Send data to the worker threads
> +    pub fn send(&self, input: I) -> Result<(), Error> {
> +        self.input.as_ref().unwrap().send(input)?;

Please add a comment why .unwrap is okay here or bubble some error up

> +        Ok(())
> +    }
> +
> +    /// Wait for worker threads to complete and check for errors
> +    pub fn complete(mut self) -> Result<(), Error> {
> +        let input = self.input.take().unwrap();
> +        let abort = Arc::clone(&input.abort);
> +        check_abort(&abort)?;
> +        drop(input);
> +
> +        let msg_list = self.join_threads();
> +
> +        // an error might be encountered while waiting for the join
> +        check_abort(&abort)?;
> +
> +        if msg_list.is_empty() {
> +            return Ok(());
> +        }
> +        Err(format_err!("{}", msg_list.join("\n")))

I'd rather

if !msg_list.is_empty() {
    bail!("{}", msg_list.join('\n'));
}

Ok(())

> +    }
> +
> +    fn join_threads(&mut self) -> Vec<String> {
> +        let mut msg_list = Vec::new();
> +
> +        let mut i = 0;
> +        while let Some(handle) = self.handles.pop() {
> +            if let Err(panic) = handle.join() {
> +                if let Some(panic_msg) = panic.downcast_ref::<&str>() {
> +                    msg_list.push(format!("thread {} ({i}) panicked: {panic_msg}", self.name));
> +                } else if let Some(panic_msg) = panic.downcast_ref::<String>() {
> +                    msg_list.push(format!("thread {} ({i}) panicked: {panic_msg}", self.name));
> +                } else {
> +                    msg_list.push(format!("thread {} ({i}) panicked", self.name));
> +                }
> +            }
> +            i += 1;
> +        }
> +        msg_list
> +    }
> +}
> +
> +// Note: We make sure that all threads will be joined
> +impl<I> Drop for ParallelHandler<I> {
> +    fn drop(&mut self) {
> +        drop(self.input.take());
> +        while let Some(handle) = self.handles.pop() {
> +            let _ = handle.join();
> +        }
> +    }
> +}
> diff --git a/wrapper.h b/wrapper.h
> new file mode 100644
> index 0000000..64d0aa6
> --- /dev/null
> +++ b/wrapper.h
> @@ -0,0 +1 @@
> +#include <rrd.h>



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH proxmox-rrd-migration-tool v4 3/3] add debian packaging
  2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 3/3] add debian packaging Aaron Lauterer
@ 2025-07-28 14:36   ` Lukas Wagner
  2025-07-29  9:29     ` Thomas Lamprecht
  2025-07-30 17:57   ` [pve-devel] applied: " Thomas Lamprecht
  1 sibling, 1 reply; 50+ messages in thread
From: Lukas Wagner @ 2025-07-28 14:36 UTC (permalink / raw)
  To: Proxmox VE development discussion; +Cc: pve-devel

On Sat Jul 26, 2025 at 3:05 AM CEST, Aaron Lauterer wrote:
> based on the termproxy packaging. Nothing fancy so far.
>
> Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
> ---
>
> Notes:
>     I added the links to the repos even though they don't exist yet. So if
>     the package and repo name is to change. make sure to adapt those :)
>
>  Cargo.toml           |  4 +-
>  Makefile             | 89 ++++++++++++++++++++++++++++++++++++++++++++
>  debian/changelog     |  5 +++
>  debian/control       | 27 ++++++++++++++
>  debian/copyright     | 19 ++++++++++
>  debian/docs          |  1 +
>  debian/links         |  1 +
>  debian/rules         | 30 +++++++++++++++
>  debian/source/format |  1 +
>  9 files changed, 175 insertions(+), 2 deletions(-)
>  create mode 100644 Makefile
>  create mode 100644 debian/changelog
>  create mode 100644 debian/control
>  create mode 100644 debian/copyright
>  create mode 100644 debian/docs
>  create mode 100644 debian/links
>  create mode 100755 debian/rules
>  create mode 100644 debian/source/format
>
> diff --git a/Cargo.toml b/Cargo.toml
> index a24b79c..e2d49a9 100644
> --- a/Cargo.toml
> +++ b/Cargo.toml
> @@ -1,6 +1,6 @@
>  [package]
> -name = "proxmox_rrd_migration_8-9"
> -version = "0.1.0"
> +name = "proxmox-rrd-migration-tool"
> +version = "1.0.0"
>  edition = "2021"
>  authors = [
>      "Aaron Lauterer <a.lauterer@proxmox.com>",
> diff --git a/Makefile b/Makefile
> new file mode 100644
> index 0000000..abce415
> --- /dev/null
> +++ b/Makefile
> @@ -0,0 +1,89 @@
> +include /usr/share/dpkg/default.mk
> +
> +PACKAGE="proxmox-rrd-migration-tool"
> +CRATENAME="proxmox-rrd-migration-tool"
> +
> +BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
> +ORIG_SRC_TAR=$(PACKAGE)_$(DEB_VERSION_UPSTREAM).orig.tar.gz
> +
> +DEB=$(PACKAGE)_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
> +DBG_DEB=$(PACKAGE)-dbgsym_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
> +DSC=$(PACKAGE)_$(DEB_VERSION).dsc
> +
> +CARGO ?= cargo
> +ifeq ($(BUILD_MODE), release)
> +CARGO_BUILD_ARGS += --release
> +COMPILEDIR := target/release
> +else
> +COMPILEDIR := target/debug
> +endif
> +
> +PREFIX = /usr
> +LIBEXECDIR = $(PREFIX)/libexec
> +PROXMOX_LIBEXECDIR = $(LIBEXECDIR)/proxmox
> +
> +PROXMOX_RRD_MIGRATION_TOOL_BIN := $(addprefix $(COMPILEDIR)/,proxmox-rrd-migration-tool)
> +
> +all:
> +
> +install: $(PROXMOX_RRD_MIGRATION_TOOL_BIN)
> +	install -dm755 $(DESTDIR)$(PROXMOX_LIBEXECDIR)
> +	install -m755 $(PROXMOX_RRD_MIGRATION_TOOL_BIN) $(DESTDIR)$(PROXMOX_LIBEXECDIR)/
> +
> +$(PROXMOX_RRD_MIGRATION_TOOL_BIN): .do-cargo-build
> +.do-cargo-build:
> +	$(CARGO) build $(CARGO_BUILD_ARGS)
> +	touch .do-cargo-build
> +
> +
> +.PHONY: cargo-build
> +cargo-build: .do-cargo-build
> +
> +$(BUILDDIR):
> +	rm -rf $@ $@.tmp
> +	mkdir $@.tmp
> +	cp -a debian/ src/ Makefile Cargo.toml wrapper.h build.rs $@.tmp
> +	echo "git clone git://git.proxmox.com/git/proxmox-rrd-migration-tool.git\\ngit checkout $$(git rev-parse HEAD)" \
> +	    > $@.tmp/debian/SOURCE
> +	mv $@.tmp $@
> +
> +
> +$(ORIG_SRC_TAR): $(BUILDDIR)
> +	tar czf $(ORIG_SRC_TAR) --exclude="$(BUILDDIR)/debian" $(BUILDDIR)
> +
> +.PHONY: deb
> +deb: $(DEB)
> +$(DEB) $(DBG_DEB) &: $(BUILDDIR)
> +	cd $(BUILDDIR); dpkg-buildpackage -b -uc -us
> +	lintian $(DEB)
> +	@echo $(DEB)
> +
> +.PHONY: dsc
> +dsc:
> +	rm -rf $(DSC) $(BUILDDIR)
> +	$(MAKE) $(DSC)
> +	lintian $(DSC)
> +
> +$(DSC): $(BUILDDIR) $(ORIG_SRC_TAR)
> +	cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d
> +
> +sbuild: $(DSC)
> +	sbuild $(DSC)
> +
> +.PHONY: upload
> +upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
> +upload: $(DEB) $(DBG_DEB)
> +	tar cf - $(DEB) $(DBG_DEB) |ssh -X repoman@repo.proxmox.com -- upload --product pve --dist $(UPLOAD_DIST) --arch $(DEB_HOST_ARCH)
> +
> +.PHONY: clean distclean
> +distclean: clean
> +clean:
> +	$(CARGO) clean
> +	rm -rf $(PACKAGE)-[0-9]*/ build/
> +	rm -f *.deb *.changes *.dsc *.tar.* *.buildinfo *.build .do-cargo-build
> +	rm -rf tmp_tests
> +	rm -rf target
> +
> +.PHONY: dinstall
> +dinstall: deb
> +	dpkg -i $(DEB)
> diff --git a/debian/changelog b/debian/changelog
> new file mode 100644
> index 0000000..b82648a
> --- /dev/null
> +++ b/debian/changelog
> @@ -0,0 +1,5 @@
> +proxmox-rrd-migration-tool (1.0.0) unstable; urgency=medium
> +
> +  * Initial release.
> +
> + -- Proxmox Support Team <support@proxmox.com>  Mon, 21 Jul 2025 13:56:37 +0200
> diff --git a/debian/control b/debian/control
> new file mode 100644
> index 0000000..8f26878
> --- /dev/null
> +++ b/debian/control
> @@ -0,0 +1,27 @@
> +Source: proxmox-rrd-migration-tool
> +Section: admin
> +Priority: optional
> +Build-Depends: cargo:native,
> +               debhelper-compat (= 13),
> +               dh-cargo (>= 25),
> +               librust-anyhow-1+default-dev,
> +               librust-bindgen-dev,
> +               librust-libc-0.2+default-dev (>= 0.2.107-~~),
> +               librust-pico-args-0.5+default-dev,
> +               librust-pkg-config-dev,
> +               librust-proxmox-async-dev,
> +               libstd-rust-dev,
> +               rustc:native,

This seems to be missing the pretty-assertions dependency, although I'm
not super sure how we handle test-only, 'dev-dependencies' in
d/control, so maybe this is correct after all...

> +Maintainer: Proxmox Support Team <support@proxmox.com>
> +Standards-Version: 4.6.1
> +Vcs-Git: git://git.proxmox.com/git/proxmox-rrd-migration-tool.git
> +Vcs-Browser: https://git.proxmox.com/?p=proxmox-rrd-migration-tool.git;a=summary
> +Homepage: https://www.proxmox.com
> +Rules-Requires-Root: no
> +
> +Package: proxmox-rrd-migration-tool
> +Architecture: any
> +Multi-Arch: allowed
> +Depends: ${misc:Depends}, ${shlibs:Depends},
> +Description: Tool to migrate RRD data on Proxmox VE hosts from pre version 8
> +  to new version 9 files.
> diff --git a/debian/copyright b/debian/copyright
> new file mode 100644
> index 0000000..451848c
> --- /dev/null
> +++ b/debian/copyright
> @@ -0,0 +1,19 @@
> +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
> +Source: https://git.proxmox.com/git/proxmox-rrd-migration-tool.git;a=summary
> +
> +Files:
> + *
> +Copyright: 2017 - 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
> +License: AGPL-3.0-or-later
> + This program is free software: you can redistribute it and/or modify it under
> + the terms of the GNU Affero General Public License as published by the Free
> + Software Foundation, either version 3 of the License, or (at your option) any
> + later version.
> + .
> + This program is distributed in the hope that it will be useful, but WITHOUT
> + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
> + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
> + details.
> + .
> + You should have received a copy of the GNU Affero General Public License along
> + with this program. If not, see <https://www.gnu.org/licenses/>.
> diff --git a/debian/docs b/debian/docs
> new file mode 100644
> index 0000000..8696672
> --- /dev/null
> +++ b/debian/docs
> @@ -0,0 +1 @@
> +debian/SOURCE
> diff --git a/debian/links b/debian/links
> new file mode 100644
> index 0000000..9e59b57
> --- /dev/null
> +++ b/debian/links
> @@ -0,0 +1 @@
> +usr/libexec/proxmox/proxmox-rrd-migration-tool usr/bin/proxmox-rrd-migration-tool

As far as I understand this is a tool that should be called directly by
a user, right? In that case I'd not add the symlink but alter the
Makefile so that it installs it to /usr/bin/ right away.


> diff --git a/debian/rules b/debian/rules
> new file mode 100755
> index 0000000..ec264eb
> --- /dev/null
> +++ b/debian/rules
> @@ -0,0 +1,30 @@
> +#!/usr/bin/make -f
> +# See debhelper(7) (uncomment to enable)
> +# output every command that modifies files on the build system.
> +DH_VERBOSE = 1
> +
> +include /usr/share/dpkg/pkg-info.mk
> +include /usr/share/rustc/architecture.mk
> +
> +export BUILD_MODE=release
> +
> +CARGO=/usr/share/cargo/bin/cargo
> +
> +export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
> +export DEB_HOST_RUST_TYPE DEB_HOST_GNU_TYPE
> +export CARGO_HOME = $(CURDIR)/debian/cargo_home
> +
> +export DEB_CARGO_CRATE=proxmox-rrd-migration-tool_$(DEB_VERSION_UPSTREAM)
> +export DEB_CARGO_PACKAGE=proxmox-rrd-migration-tool
> +
> +%:
> +	dh $@
> +
> +override_dh_auto_configure:
> +	@perl -ne 'if (/^version\s*=\s*"(\d+(?:\.\d+)+)"/) { my $$v_cargo = $$1; my $$v_deb = "$(DEB_VERSION_UPSTREAM)"; \
> +	    die "ERROR: d/changelog <-> Cargo.toml version mismatch: $$v_cargo != $$v_deb\n" if $$v_cargo ne $$v_deb; exit(0); }' Cargo.toml
> +	$(CARGO) prepare-debian $(CURDIR)/debian/cargo_registry --link-from-system
> +	dh_auto_configure
> +
> +override_dh_missing:
> +	dh_missing --fail-missing
> diff --git a/debian/source/format b/debian/source/format
> new file mode 100644
> index 0000000..89ae9db
> --- /dev/null
> +++ b/debian/source/format
> @@ -0,0 +1 @@
> +3.0 (native)



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (30 preceding siblings ...)
  2025-07-26  1:06 ` [pve-devel] [PATCH container v4 2/2] rrddata: use new pve-vm-9.0 rrd location if file is present Aaron Lauterer
@ 2025-07-28 14:42 ` Thomas Lamprecht
  2025-07-29 12:19 ` Lukas Wagner
  2025-07-31  4:12 ` [pve-devel] applied: " Thomas Lamprecht
  33 siblings, 0 replies; 50+ messages in thread
From: Thomas Lamprecht @ 2025-07-28 14:42 UTC (permalink / raw)
  To: Proxmox VE development discussion, Aaron Lauterer

Am 26.07.25 um 03:06 schrieb Aaron Lauterer:
> KNOWN ISSUES:
> * on a live system, renaming the source RRD files to FILE.old doesn't seem to
> work as expected and besides the renamed ones, new ones without the .old prefix
> show up again. I suspect some interaction with rrdached and/or pmxcfs receiving
> new data.

Good that we did the rename on migration, as otherwise this might have
gone unnoticed.

The cleanest and simplest solution here might be to trigger the migration
only on boot, i.e. before rrdcached/pve-cluster starts up and can do any
RRD related stuff. While I'm not a huge fan of doing such migrations, it
is node local and only affects metrics and the old data is still there
in the renamed files, so no high fallout risk.


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH proxmox-rrd-migration-tool v4 2/3] add first tests
  2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 2/3] add first tests Aaron Lauterer
@ 2025-07-28 14:52   ` Lukas Wagner
  0 siblings, 0 replies; 50+ messages in thread
From: Lukas Wagner @ 2025-07-28 14:52 UTC (permalink / raw)
  To: Proxmox VE development discussion; +Cc: pve-devel

Nice, it's always great to see some tests.
Only tiny nits inline, so still:

Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>

On Sat Jul 26, 2025 at 3:05 AM CEST, Aaron Lauterer wrote:
> they are not pretty, but we now can test the following:
>
> * resulting RRD file matches the expected rrdinfo output
>   By running the resulting binary within 'faketime'
>   -> had to filter out some lines that change with each iteration
> * .old files are ignored
> * processed files are renamed to have the .old appendix
> * that a follow up run won't find anything to migrate
> * that an RRD file for a VM that was created during the migration will
>   be migrated in a second run
>
> We also set RUST_TEST_THREADS to 1 in .cargo/config.toml as they
> currently all operate on the same tmp directory.
>
> Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
> ---
>  .cargo/config.toml                            |   3 +
>  .gitignore                                    |   1 +
>  Cargo.toml                                    |   2 +
>  tests/migration.rs                            | 185 +++++++
>  tests/resources/compare/pve-node-9.0_testnode | 501 ++++++++++++++++++
>  .../compare/pve-storage-9.0_testnode_iso      |  93 ++++
>  tests/resources/compare/pve-vm-9.0_100        | 453 ++++++++++++++++
>  tests/resources/compare/second_empty_run      |   8 +
>  .../resources/compare/second_run_with_missed  |   7 +
>  tests/resources/resourcelists/.members        |  10 +
>  tests/resources/resourcelists/.vmlist         |   7 +
>  .../resources/source/pve2-node/othernode.old  | Bin 0 -> 81008 bytes
>  tests/resources/source/pve2-node/testnode     | Bin 0 -> 81008 bytes
>  .../source/pve2-storage/testnode/foo.old      | Bin 0 -> 14688 bytes
>  .../source/pve2-storage/testnode/iso          | Bin 0 -> 14688 bytes
>  tests/resources/source/pve2-vm/100            | Bin 0 -> 67744 bytes
>  tests/resources/source/pve2-vm/400            | Bin 0 -> 67744 bytes
>  tests/resources/source/pve2-vm/500.old        | Bin 0 -> 67744 bytes
>  tests/utils.rs                                | 117 ++++
>  19 files changed, 1387 insertions(+)
>  create mode 100644 tests/migration.rs
>  create mode 100644 tests/resources/compare/pve-node-9.0_testnode
>  create mode 100644 tests/resources/compare/pve-storage-9.0_testnode_iso
>  create mode 100644 tests/resources/compare/pve-vm-9.0_100
>  create mode 100644 tests/resources/compare/second_empty_run
>  create mode 100644 tests/resources/compare/second_run_with_missed
>  create mode 100644 tests/resources/resourcelists/.members
>  create mode 100644 tests/resources/resourcelists/.vmlist
>  create mode 100644 tests/resources/source/pve2-node/othernode.old
>  create mode 100644 tests/resources/source/pve2-node/testnode
>  create mode 100644 tests/resources/source/pve2-storage/testnode/foo.old
>  create mode 100644 tests/resources/source/pve2-storage/testnode/iso
>  create mode 100644 tests/resources/source/pve2-vm/100
>  create mode 100644 tests/resources/source/pve2-vm/400
>  create mode 100644 tests/resources/source/pve2-vm/500.old
>  create mode 100644 tests/utils.rs
>
> diff --git a/.cargo/config.toml b/.cargo/config.toml
> index 3b5b6e4..cf8bc1e 100644
> --- a/.cargo/config.toml
> +++ b/.cargo/config.toml
> @@ -3,3 +3,6 @@
>  directory = "/usr/share/cargo/registry"
>  [source.crates-io]
>  replace-with = "debian-packages"
> +[env]
> +# as they currently use the same tmp_tests dir to perform the tests
> +RUST_TEST_THREADS = "1"
> diff --git a/.gitignore b/.gitignore
> index 06ac1a1..d8d3016 100644
> --- a/.gitignore
> +++ b/.gitignore
> @@ -7,3 +7,4 @@
>  target/
>  /Cargo.lock
>  /proxmox-rrd-migration-tool-[0-9]*/
> +/tmp_tests
> diff --git a/Cargo.toml b/Cargo.toml
> index d3523f3..a24b79c 100644
> --- a/Cargo.toml
> +++ b/Cargo.toml
> @@ -18,3 +18,5 @@ crossbeam-channel = "0.5"
>  [build-dependencies]
>  bindgen = "0.66.1"
>  pkg-config = "0.3"
> +[dev-dependencies]
> +pretty_assertions = "1.4"
> diff --git a/tests/migration.rs b/tests/migration.rs
> new file mode 100644
> index 0000000..ea425d5
> --- /dev/null
> +++ b/tests/migration.rs
> @@ -0,0 +1,185 @@
> +use anyhow::Error;
> +use pretty_assertions::assert_eq;
> +use std::{
> +    fs,
> +    path::{Path, PathBuf},
> +    process::Command,
> +};
> +
> +mod utils;
> +
> +use utils::{TMPDIR, TMPDIR_RESOURCELISTS, TMPDIR_SOURCE_BASEDIR, TMPDIR_TARGET};
> +
> +const TARGET_SUBDIR_NODE: &str = "pve-node-9.0";
> +const TARGET_SUBDIR_GUEST: &str = "pve-vm-9.0";
> +const TARGET_SUBDIR_STORAGE: &str = "pve-storage-9.0";
> +
> +#[test]
> +fn migration() {
> +    utils::test_prepare();
> +
> +    let target_dir_guests: PathBuf = [TMPDIR_TARGET, TARGET_SUBDIR_GUEST].iter().collect();
> +    let target_dir_nodes: PathBuf = [TMPDIR_TARGET, TARGET_SUBDIR_NODE].iter().collect();
> +    let target_dir_storage: PathBuf = [TMPDIR_TARGET, TARGET_SUBDIR_STORAGE].iter().collect();

Same thing here as mentioned in the first patch.

> +
> +    // first test, compare resulting rrd files
> +    Command::new("faketime")
> +        .arg("2025-08-01 00:00:00")
> +        .arg(utils::migration_tool_path())
> +        .arg("--migrate")
> +        .arg("--source")
> +        .arg(TMPDIR_SOURCE_BASEDIR)
> +        .arg("--target")
> +        .arg(TMPDIR_TARGET)
> +        .arg("--resources")
> +        .arg(TMPDIR_RESOURCELISTS)
> +        .output()
> +        .expect("failed to execute proxmox-rrd-migration-tool");
> +
> +    // assert target files as we expect them
> +    assert!(Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_NODE}/testnode").as_str()).exists());
> +    assert!(Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_GUEST}/100").as_str()).exists());
> +    assert!(!Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_GUEST}/400").as_str()).exists());
> +    assert!(!Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_GUEST}/400.old").as_str()).exists());
> +    assert!(!Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_GUEST}/500.old").as_str()).exists());
> +    assert!(
> +        Path::new(format!("{TMPDIR_TARGET}/{TARGET_SUBDIR_STORAGE}/testnode/iso").as_str())
> +            .exists()
> +    );
> +    assert!(Path::new(format!("{TMPDIR_SOURCE_BASEDIR}/pve2-vm/100.old").as_str()).exists());
> +    assert!(Path::new(format!("{TMPDIR_SOURCE_BASEDIR}/pve2-vm/400.old").as_str()).exists());

You can use & instead of .as_str here, makes it a  bit shorter and
easier to read :)

> +
> +    // compare
> +    utils::compare_results("node", &target_dir_nodes, &TARGET_SUBDIR_NODE);
> +
> +    utils::compare_results("guest", &target_dir_guests, &TARGET_SUBDIR_GUEST);
> +
> +    // storage has another layer of directories per node over which we need to iterate
> +    fs::read_dir(&target_dir_storage)
> +        .expect("could not read target storage dir")
> +        .filter(|f| f.is_ok())
> +        .map(|f| f.unwrap().path())
> +        .filter(|f| f.is_dir())

you can use filter_map here

> +        .try_for_each(|node| {
> +            let mut source_storage_subdir = target_dir_storage.clone();
> +            source_storage_subdir.push(node.file_name().unwrap());
> +
> +            let mut target_storage_subdir = target_dir_storage.clone();
> +            target_storage_subdir.push(node.file_name().unwrap());
> +
> +            utils::compare_results(
> +                "storage",
> +                &source_storage_subdir,
> +                format!(
> +                    "{TARGET_SUBDIR_STORAGE}_{}",
> +                    node.file_name().unwrap().to_string_lossy()
> +                )
> +                .as_str(),
> +            );
> +            Ok::<(), Error>(())
> +        })
> +        .expect("Error running storage test");
> +}
> +#[test]
> +fn migration_second_empty_run() {
> +    utils::test_prepare();
> +
> +    // run initial migration
> +    Command::new("faketime")
> +        .arg("2025-08-01 00:00:00")
> +        .arg(utils::migration_tool_path())
> +        .arg("--migrate")
> +        .arg("--source")
> +        .arg(TMPDIR_SOURCE_BASEDIR)
> +        .arg("--target")
> +        .arg(TMPDIR_TARGET)
> +        .arg("--resources")
> +        .arg(TMPDIR_RESOURCELISTS)
> +        .output()
> +        .expect("failed to execute proxmox-rrd-migration-tool");
> +
> +    // check if output skips all currently existing files
> +    let output = Command::new("faketime")
> +        .arg("2025-08-01 00:00:00")
> +        .arg(utils::migration_tool_path())
> +        .arg("--threads")
> +        .arg("2")
> +        .arg("--migrate")
> +        .arg("--source")
> +        .arg(TMPDIR_SOURCE_BASEDIR)
> +        .arg("--target")
> +        .arg(TMPDIR_TARGET)
> +        .arg("--resources")
> +        .arg(TMPDIR_RESOURCELISTS)
> +        .output()
> +        .expect("failed to execute proxmox-rrd-migration-tool");
> +    let expected_path: PathBuf = [TMPDIR, "resources", "compare", "second_empty_run"]
> +        .iter()
> +        .collect();
> +
> +    let expected =
> +        fs::read_to_string(expected_path).expect("could not read compare file for skip all");
> +
> +    assert_eq!(
> +        expected,
> +        String::from_utf8(output.stdout).expect("could not parse output")
> +    );
> +}
> +
> +#[test]
> +fn migration_second_run_with_missed_files() {
> +    utils::test_prepare();
> +
> +    // run initial migration
> +    Command::new("faketime")
> +        .arg("2025-08-01 00:00:00")
> +        .arg(utils::migration_tool_path())
> +        .arg("--migrate")
> +        .arg("--source")
> +        .arg(TMPDIR_SOURCE_BASEDIR)
> +        .arg("--target")
> +        .arg(TMPDIR_TARGET)
> +        .arg("--resources")
> +        .arg(TMPDIR_RESOURCELISTS)
> +        .output()
> +        .expect("failed to execute proxmox-rrd-migration-tool");
> +
> +    let src_vm = format!("{TMPDIR_SOURCE_BASEDIR}/pve2-vm/100.old");
> +    let target_vm = format!("{TMPDIR_SOURCE_BASEDIR}/pve2-vm/101");
> +
> +    Command::new("cp")
> +        .args([src_vm, target_vm])
> +        .output()
> +        .expect("copy 101 rrd file");
> +
> +    // check if output skips all currently existing files
> +    let output = Command::new("faketime")
> +        .arg("2025-08-01 00:00:00")
> +        .arg(utils::migration_tool_path())
> +        .arg("--threads")
> +        .arg("2")
> +        .arg("--migrate")
> +        .arg("--source")
> +        .arg(TMPDIR_SOURCE_BASEDIR)
> +        .arg("--target")
> +        .arg(TMPDIR_TARGET)
> +        .arg("--resources")
> +        .arg(TMPDIR_RESOURCELISTS)
> +        .output()
> +        .expect("failed to execute proxmox-rrd-migration-tool");
> +
> +    let expected_path: PathBuf = [TMPDIR, "resources", "compare", "second_run_with_missed"]
> +        .iter()
> +        .collect();
> +
> +    let expected = fs::read_to_string(expected_path.as_path())
> +        .expect("could not read compare file for skip all");
> +
> +    // drop last line from output as it contains timing information which can change between tests
> +    let output = utils::drop_last_line(output.stdout);
> +
> +    println!("OUTPUT:\n{}", output);
> +    println!("EXPECTED:\n{}", expected);
> +
> +    assert_eq!(expected, output);
> +}
> diff --git a/tests/resources/compare/pve-node-9.0_testnode b/tests/resources/compare/pve-node-9.0_testnode
> new file mode 100644
> index 0000000..ebd09a4
> --- /dev/null
> +++ b/tests/resources/compare/pve-node-9.0_testnode
> @@ -0,0 +1,501 @@
> +filename = "tmp_tests/target/pve-node-9.0/testnode"
> +rrd_version = "0003"
> +step = 60
> +last_update = 1753999190
> +header_size = 17736
> +ds[loadavg].index = 0
> +ds[loadavg].type = "GAUGE"
> +ds[loadavg].minimal_heartbeat = 120
> +ds[loadavg].min = 0.0000000000e+00
> +ds[loadavg].max = NaN
> +ds[loadavg].last_ds = "U"
> +ds[loadavg].value = NaN
> +ds[loadavg].unknown_sec = 50
> +ds[maxcpu].index = 1
> +ds[maxcpu].type = "GAUGE"
> +ds[maxcpu].minimal_heartbeat = 120
> +ds[maxcpu].min = 0.0000000000e+00
> +ds[maxcpu].max = NaN
> +ds[maxcpu].last_ds = "U"
> +ds[maxcpu].value = NaN
> +ds[maxcpu].unknown_sec = 50
> +ds[cpu].index = 2
> +ds[cpu].type = "GAUGE"
> +ds[cpu].minimal_heartbeat = 120
> +ds[cpu].min = 0.0000000000e+00
> +ds[cpu].max = NaN
> +ds[cpu].last_ds = "U"
> +ds[cpu].value = NaN
> +ds[cpu].unknown_sec = 50
> +ds[iowait].index = 3
> +ds[iowait].type = "GAUGE"
> +ds[iowait].minimal_heartbeat = 120
> +ds[iowait].min = 0.0000000000e+00
> +ds[iowait].max = NaN
> +ds[iowait].last_ds = "U"
> +ds[iowait].value = NaN
> +ds[iowait].unknown_sec = 50
> +ds[memtotal].index = 4
> +ds[memtotal].type = "GAUGE"
> +ds[memtotal].minimal_heartbeat = 120
> +ds[memtotal].min = 0.0000000000e+00
> +ds[memtotal].max = NaN
> +ds[memtotal].last_ds = "U"
> +ds[memtotal].value = NaN
> +ds[memtotal].unknown_sec = 50
> +ds[memused].index = 5
> +ds[memused].type = "GAUGE"
> +ds[memused].minimal_heartbeat = 120
> +ds[memused].min = 0.0000000000e+00
> +ds[memused].max = NaN
> +ds[memused].last_ds = "U"
> +ds[memused].value = NaN
> +ds[memused].unknown_sec = 50
> +ds[swaptotal].index = 6
> +ds[swaptotal].type = "GAUGE"
> +ds[swaptotal].minimal_heartbeat = 120
> +ds[swaptotal].min = 0.0000000000e+00
> +ds[swaptotal].max = NaN
> +ds[swaptotal].last_ds = "U"
> +ds[swaptotal].value = NaN
> +ds[swaptotal].unknown_sec = 50
> +ds[swapused].index = 7
> +ds[swapused].type = "GAUGE"
> +ds[swapused].minimal_heartbeat = 120
> +ds[swapused].min = 0.0000000000e+00
> +ds[swapused].max = NaN
> +ds[swapused].last_ds = "U"
> +ds[swapused].value = NaN
> +ds[swapused].unknown_sec = 50
> +ds[roottotal].index = 8
> +ds[roottotal].type = "GAUGE"
> +ds[roottotal].minimal_heartbeat = 120
> +ds[roottotal].min = 0.0000000000e+00
> +ds[roottotal].max = NaN
> +ds[roottotal].last_ds = "U"
> +ds[roottotal].value = NaN
> +ds[roottotal].unknown_sec = 50
> +ds[rootused].index = 9
> +ds[rootused].type = "GAUGE"
> +ds[rootused].minimal_heartbeat = 120
> +ds[rootused].min = 0.0000000000e+00
> +ds[rootused].max = NaN
> +ds[rootused].last_ds = "U"
> +ds[rootused].value = NaN
> +ds[rootused].unknown_sec = 50
> +ds[netin].index = 10
> +ds[netin].type = "DERIVE"
> +ds[netin].minimal_heartbeat = 120
> +ds[netin].min = 0.0000000000e+00
> +ds[netin].max = NaN
> +ds[netin].last_ds = "U"
> +ds[netin].value = NaN
> +ds[netin].unknown_sec = 50
> +ds[netout].index = 11
> +ds[netout].type = "DERIVE"
> +ds[netout].minimal_heartbeat = 120
> +ds[netout].min = 0.0000000000e+00
> +ds[netout].max = NaN
> +ds[netout].last_ds = "U"
> +ds[netout].value = NaN
> +ds[netout].unknown_sec = 50
> +ds[memfree].index = 12
> +ds[memfree].type = "GAUGE"
> +ds[memfree].minimal_heartbeat = 120
> +ds[memfree].min = 0.0000000000e+00
> +ds[memfree].max = NaN
> +ds[memfree].last_ds = "U"
> +ds[memfree].value = NaN
> +ds[memfree].unknown_sec = 50
> +ds[arcsize].index = 13
> +ds[arcsize].type = "GAUGE"
> +ds[arcsize].minimal_heartbeat = 120
> +ds[arcsize].min = 0.0000000000e+00
> +ds[arcsize].max = NaN
> +ds[arcsize].last_ds = "U"
> +ds[arcsize].value = NaN
> +ds[arcsize].unknown_sec = 50
> +ds[pressurecpusome].index = 14
> +ds[pressurecpusome].type = "GAUGE"
> +ds[pressurecpusome].minimal_heartbeat = 120
> +ds[pressurecpusome].min = 0.0000000000e+00
> +ds[pressurecpusome].max = NaN
> +ds[pressurecpusome].last_ds = "U"
> +ds[pressurecpusome].value = NaN
> +ds[pressurecpusome].unknown_sec = 50
> +ds[pressureiosome].index = 15
> +ds[pressureiosome].type = "GAUGE"
> +ds[pressureiosome].minimal_heartbeat = 120
> +ds[pressureiosome].min = 0.0000000000e+00
> +ds[pressureiosome].max = NaN
> +ds[pressureiosome].last_ds = "U"
> +ds[pressureiosome].value = NaN
> +ds[pressureiosome].unknown_sec = 50
> +ds[pressureiofull].index = 16
> +ds[pressureiofull].type = "GAUGE"
> +ds[pressureiofull].minimal_heartbeat = 120
> +ds[pressureiofull].min = 0.0000000000e+00
> +ds[pressureiofull].max = NaN
> +ds[pressureiofull].last_ds = "U"
> +ds[pressureiofull].value = NaN
> +ds[pressureiofull].unknown_sec = 50
> +ds[pressurememorysome].index = 17
> +ds[pressurememorysome].type = "GAUGE"
> +ds[pressurememorysome].minimal_heartbeat = 120
> +ds[pressurememorysome].min = 0.0000000000e+00
> +ds[pressurememorysome].max = NaN
> +ds[pressurememorysome].last_ds = "U"
> +ds[pressurememorysome].value = NaN
> +ds[pressurememorysome].unknown_sec = 50
> +ds[pressurememoryfull].index = 18
> +ds[pressurememoryfull].type = "GAUGE"
> +ds[pressurememoryfull].minimal_heartbeat = 120
> +ds[pressurememoryfull].min = 0.0000000000e+00
> +ds[pressurememoryfull].max = NaN
> +ds[pressurememoryfull].last_ds = "U"
> +ds[pressurememoryfull].value = NaN
> +ds[pressurememoryfull].unknown_sec = 50
> +rra[0].cf = "AVERAGE"
> +rra[0].rows = 1440
> +rra[0].cur_row = 1184
> +rra[0].pdp_per_row = 1
> +rra[0].xff = 5.0000000000e-01
> +rra[0].cdp_prep[0].value = NaN
> +rra[0].cdp_prep[0].unknown_datapoints = 0
> +rra[0].cdp_prep[1].value = NaN
> +rra[0].cdp_prep[1].unknown_datapoints = 0
> +rra[0].cdp_prep[2].value = NaN
> +rra[0].cdp_prep[2].unknown_datapoints = 0
> +rra[0].cdp_prep[3].value = NaN
> +rra[0].cdp_prep[3].unknown_datapoints = 0
> +rra[0].cdp_prep[4].value = NaN
> +rra[0].cdp_prep[4].unknown_datapoints = 0
> +rra[0].cdp_prep[5].value = NaN
> +rra[0].cdp_prep[5].unknown_datapoints = 0
> +rra[0].cdp_prep[6].value = NaN
> +rra[0].cdp_prep[6].unknown_datapoints = 0
> +rra[0].cdp_prep[7].value = NaN
> +rra[0].cdp_prep[7].unknown_datapoints = 0
> +rra[0].cdp_prep[8].value = NaN
> +rra[0].cdp_prep[8].unknown_datapoints = 0
> +rra[0].cdp_prep[9].value = NaN
> +rra[0].cdp_prep[9].unknown_datapoints = 0
> +rra[0].cdp_prep[10].value = NaN
> +rra[0].cdp_prep[10].unknown_datapoints = 0
> +rra[0].cdp_prep[11].value = NaN
> +rra[0].cdp_prep[11].unknown_datapoints = 0
> +rra[0].cdp_prep[12].value = NaN
> +rra[0].cdp_prep[12].unknown_datapoints = 0
> +rra[0].cdp_prep[13].value = NaN
> +rra[0].cdp_prep[13].unknown_datapoints = 0
> +rra[0].cdp_prep[14].value = NaN
> +rra[0].cdp_prep[14].unknown_datapoints = 0
> +rra[0].cdp_prep[15].value = NaN
> +rra[0].cdp_prep[15].unknown_datapoints = 0
> +rra[0].cdp_prep[16].value = NaN
> +rra[0].cdp_prep[16].unknown_datapoints = 0
> +rra[0].cdp_prep[17].value = NaN
> +rra[0].cdp_prep[17].unknown_datapoints = 0
> +rra[0].cdp_prep[18].value = NaN
> +rra[0].cdp_prep[18].unknown_datapoints = 0
> +rra[1].cf = "AVERAGE"
> +rra[1].rows = 1440
> +rra[1].cur_row = 1388
> +rra[1].pdp_per_row = 30
> +rra[1].xff = 5.0000000000e-01
> +rra[1].cdp_prep[0].value = 0.0000000000e+00
> +rra[1].cdp_prep[0].unknown_datapoints = 29
> +rra[1].cdp_prep[1].value = 0.0000000000e+00
> +rra[1].cdp_prep[1].unknown_datapoints = 29
> +rra[1].cdp_prep[2].value = 0.0000000000e+00
> +rra[1].cdp_prep[2].unknown_datapoints = 29
> +rra[1].cdp_prep[3].value = 0.0000000000e+00
> +rra[1].cdp_prep[3].unknown_datapoints = 29
> +rra[1].cdp_prep[4].value = 0.0000000000e+00
> +rra[1].cdp_prep[4].unknown_datapoints = 29
> +rra[1].cdp_prep[5].value = 0.0000000000e+00
> +rra[1].cdp_prep[5].unknown_datapoints = 29
> +rra[1].cdp_prep[6].value = 0.0000000000e+00
> +rra[1].cdp_prep[6].unknown_datapoints = 29
> +rra[1].cdp_prep[7].value = 0.0000000000e+00
> +rra[1].cdp_prep[7].unknown_datapoints = 29
> +rra[1].cdp_prep[8].value = 0.0000000000e+00
> +rra[1].cdp_prep[8].unknown_datapoints = 29
> +rra[1].cdp_prep[9].value = 0.0000000000e+00
> +rra[1].cdp_prep[9].unknown_datapoints = 29
> +rra[1].cdp_prep[10].value = 0.0000000000e+00
> +rra[1].cdp_prep[10].unknown_datapoints = 29
> +rra[1].cdp_prep[11].value = 0.0000000000e+00
> +rra[1].cdp_prep[11].unknown_datapoints = 29
> +rra[1].cdp_prep[12].value = 0.0000000000e+00
> +rra[1].cdp_prep[12].unknown_datapoints = 29
> +rra[1].cdp_prep[13].value = 0.0000000000e+00
> +rra[1].cdp_prep[13].unknown_datapoints = 29
> +rra[1].cdp_prep[14].value = 0.0000000000e+00
> +rra[1].cdp_prep[14].unknown_datapoints = 29
> +rra[1].cdp_prep[15].value = 0.0000000000e+00
> +rra[1].cdp_prep[15].unknown_datapoints = 29
> +rra[1].cdp_prep[16].value = 0.0000000000e+00
> +rra[1].cdp_prep[16].unknown_datapoints = 29
> +rra[1].cdp_prep[17].value = 0.0000000000e+00
> +rra[1].cdp_prep[17].unknown_datapoints = 29
> +rra[1].cdp_prep[18].value = 0.0000000000e+00
> +rra[1].cdp_prep[18].unknown_datapoints = 29
> +rra[2].cf = "AVERAGE"
> +rra[2].rows = 1440
> +rra[2].cur_row = 130
> +rra[2].pdp_per_row = 360
> +rra[2].xff = 5.0000000000e-01
> +rra[2].cdp_prep[0].value = 0.0000000000e+00
> +rra[2].cdp_prep[0].unknown_datapoints = 239
> +rra[2].cdp_prep[1].value = 0.0000000000e+00
> +rra[2].cdp_prep[1].unknown_datapoints = 239
> +rra[2].cdp_prep[2].value = 0.0000000000e+00
> +rra[2].cdp_prep[2].unknown_datapoints = 239
> +rra[2].cdp_prep[3].value = 0.0000000000e+00
> +rra[2].cdp_prep[3].unknown_datapoints = 239
> +rra[2].cdp_prep[4].value = 0.0000000000e+00
> +rra[2].cdp_prep[4].unknown_datapoints = 239
> +rra[2].cdp_prep[5].value = 0.0000000000e+00
> +rra[2].cdp_prep[5].unknown_datapoints = 239
> +rra[2].cdp_prep[6].value = 0.0000000000e+00
> +rra[2].cdp_prep[6].unknown_datapoints = 239
> +rra[2].cdp_prep[7].value = 0.0000000000e+00
> +rra[2].cdp_prep[7].unknown_datapoints = 239
> +rra[2].cdp_prep[8].value = 0.0000000000e+00
> +rra[2].cdp_prep[8].unknown_datapoints = 239
> +rra[2].cdp_prep[9].value = 0.0000000000e+00
> +rra[2].cdp_prep[9].unknown_datapoints = 239
> +rra[2].cdp_prep[10].value = 0.0000000000e+00
> +rra[2].cdp_prep[10].unknown_datapoints = 239
> +rra[2].cdp_prep[11].value = 0.0000000000e+00
> +rra[2].cdp_prep[11].unknown_datapoints = 239
> +rra[2].cdp_prep[12].value = 0.0000000000e+00
> +rra[2].cdp_prep[12].unknown_datapoints = 239
> +rra[2].cdp_prep[13].value = 0.0000000000e+00
> +rra[2].cdp_prep[13].unknown_datapoints = 239
> +rra[2].cdp_prep[14].value = 0.0000000000e+00
> +rra[2].cdp_prep[14].unknown_datapoints = 239
> +rra[2].cdp_prep[15].value = 0.0000000000e+00
> +rra[2].cdp_prep[15].unknown_datapoints = 239
> +rra[2].cdp_prep[16].value = 0.0000000000e+00
> +rra[2].cdp_prep[16].unknown_datapoints = 239
> +rra[2].cdp_prep[17].value = 0.0000000000e+00
> +rra[2].cdp_prep[17].unknown_datapoints = 239
> +rra[2].cdp_prep[18].value = 0.0000000000e+00
> +rra[2].cdp_prep[18].unknown_datapoints = 239
> +rra[3].cf = "AVERAGE"
> +rra[3].rows = 570
> +rra[3].cur_row = 264
> +rra[3].pdp_per_row = 10080
> +rra[3].xff = 5.0000000000e-01
> +rra[3].cdp_prep[0].value = 0.0000000000e+00
> +rra[3].cdp_prep[0].unknown_datapoints = 1319
> +rra[3].cdp_prep[1].value = 0.0000000000e+00
> +rra[3].cdp_prep[1].unknown_datapoints = 1319
> +rra[3].cdp_prep[2].value = 0.0000000000e+00
> +rra[3].cdp_prep[2].unknown_datapoints = 1319
> +rra[3].cdp_prep[3].value = 0.0000000000e+00
> +rra[3].cdp_prep[3].unknown_datapoints = 1319
> +rra[3].cdp_prep[4].value = 0.0000000000e+00
> +rra[3].cdp_prep[4].unknown_datapoints = 1319
> +rra[3].cdp_prep[5].value = 0.0000000000e+00
> +rra[3].cdp_prep[5].unknown_datapoints = 1319
> +rra[3].cdp_prep[6].value = 0.0000000000e+00
> +rra[3].cdp_prep[6].unknown_datapoints = 1319
> +rra[3].cdp_prep[7].value = 0.0000000000e+00
> +rra[3].cdp_prep[7].unknown_datapoints = 1319
> +rra[3].cdp_prep[8].value = 0.0000000000e+00
> +rra[3].cdp_prep[8].unknown_datapoints = 1319
> +rra[3].cdp_prep[9].value = 0.0000000000e+00
> +rra[3].cdp_prep[9].unknown_datapoints = 1319
> +rra[3].cdp_prep[10].value = 0.0000000000e+00
> +rra[3].cdp_prep[10].unknown_datapoints = 1319
> +rra[3].cdp_prep[11].value = 0.0000000000e+00
> +rra[3].cdp_prep[11].unknown_datapoints = 1319
> +rra[3].cdp_prep[12].value = 0.0000000000e+00
> +rra[3].cdp_prep[12].unknown_datapoints = 1319
> +rra[3].cdp_prep[13].value = 0.0000000000e+00
> +rra[3].cdp_prep[13].unknown_datapoints = 1319
> +rra[3].cdp_prep[14].value = 0.0000000000e+00
> +rra[3].cdp_prep[14].unknown_datapoints = 1319
> +rra[3].cdp_prep[15].value = 0.0000000000e+00
> +rra[3].cdp_prep[15].unknown_datapoints = 1319
> +rra[3].cdp_prep[16].value = 0.0000000000e+00
> +rra[3].cdp_prep[16].unknown_datapoints = 1319
> +rra[3].cdp_prep[17].value = 0.0000000000e+00
> +rra[3].cdp_prep[17].unknown_datapoints = 1319
> +rra[3].cdp_prep[18].value = 0.0000000000e+00
> +rra[3].cdp_prep[18].unknown_datapoints = 1319
> +rra[4].cf = "MAX"
> +rra[4].rows = 1440
> +rra[4].cur_row = 747
> +rra[4].pdp_per_row = 1
> +rra[4].xff = 5.0000000000e-01
> +rra[4].cdp_prep[0].value = NaN
> +rra[4].cdp_prep[0].unknown_datapoints = 0
> +rra[4].cdp_prep[1].value = NaN
> +rra[4].cdp_prep[1].unknown_datapoints = 0
> +rra[4].cdp_prep[2].value = NaN
> +rra[4].cdp_prep[2].unknown_datapoints = 0
> +rra[4].cdp_prep[3].value = NaN
> +rra[4].cdp_prep[3].unknown_datapoints = 0
> +rra[4].cdp_prep[4].value = NaN
> +rra[4].cdp_prep[4].unknown_datapoints = 0
> +rra[4].cdp_prep[5].value = NaN
> +rra[4].cdp_prep[5].unknown_datapoints = 0
> +rra[4].cdp_prep[6].value = NaN
> +rra[4].cdp_prep[6].unknown_datapoints = 0
> +rra[4].cdp_prep[7].value = NaN
> +rra[4].cdp_prep[7].unknown_datapoints = 0
> +rra[4].cdp_prep[8].value = NaN
> +rra[4].cdp_prep[8].unknown_datapoints = 0
> +rra[4].cdp_prep[9].value = NaN
> +rra[4].cdp_prep[9].unknown_datapoints = 0
> +rra[4].cdp_prep[10].value = NaN
> +rra[4].cdp_prep[10].unknown_datapoints = 0
> +rra[4].cdp_prep[11].value = NaN
> +rra[4].cdp_prep[11].unknown_datapoints = 0
> +rra[4].cdp_prep[12].value = NaN
> +rra[4].cdp_prep[12].unknown_datapoints = 0
> +rra[4].cdp_prep[13].value = NaN
> +rra[4].cdp_prep[13].unknown_datapoints = 0
> +rra[4].cdp_prep[14].value = NaN
> +rra[4].cdp_prep[14].unknown_datapoints = 0
> +rra[4].cdp_prep[15].value = NaN
> +rra[4].cdp_prep[15].unknown_datapoints = 0
> +rra[4].cdp_prep[16].value = NaN
> +rra[4].cdp_prep[16].unknown_datapoints = 0
> +rra[4].cdp_prep[17].value = NaN
> +rra[4].cdp_prep[17].unknown_datapoints = 0
> +rra[4].cdp_prep[18].value = NaN
> +rra[4].cdp_prep[18].unknown_datapoints = 0
> +rra[5].cf = "MAX"
> +rra[5].rows = 1440
> +rra[5].cur_row = 574
> +rra[5].pdp_per_row = 30
> +rra[5].xff = 5.0000000000e-01
> +rra[5].cdp_prep[0].value = -inf
> +rra[5].cdp_prep[0].unknown_datapoints = 29
> +rra[5].cdp_prep[1].value = -inf
> +rra[5].cdp_prep[1].unknown_datapoints = 29
> +rra[5].cdp_prep[2].value = -inf
> +rra[5].cdp_prep[2].unknown_datapoints = 29
> +rra[5].cdp_prep[3].value = -inf
> +rra[5].cdp_prep[3].unknown_datapoints = 29
> +rra[5].cdp_prep[4].value = -inf
> +rra[5].cdp_prep[4].unknown_datapoints = 29
> +rra[5].cdp_prep[5].value = -inf
> +rra[5].cdp_prep[5].unknown_datapoints = 29
> +rra[5].cdp_prep[6].value = -inf
> +rra[5].cdp_prep[6].unknown_datapoints = 29
> +rra[5].cdp_prep[7].value = -inf
> +rra[5].cdp_prep[7].unknown_datapoints = 29
> +rra[5].cdp_prep[8].value = -inf
> +rra[5].cdp_prep[8].unknown_datapoints = 29
> +rra[5].cdp_prep[9].value = -inf
> +rra[5].cdp_prep[9].unknown_datapoints = 29
> +rra[5].cdp_prep[10].value = -inf
> +rra[5].cdp_prep[10].unknown_datapoints = 29
> +rra[5].cdp_prep[11].value = -inf
> +rra[5].cdp_prep[11].unknown_datapoints = 29
> +rra[5].cdp_prep[12].value = -inf
> +rra[5].cdp_prep[12].unknown_datapoints = 29
> +rra[5].cdp_prep[13].value = -inf
> +rra[5].cdp_prep[13].unknown_datapoints = 29
> +rra[5].cdp_prep[14].value = -inf
> +rra[5].cdp_prep[14].unknown_datapoints = 29
> +rra[5].cdp_prep[15].value = -inf
> +rra[5].cdp_prep[15].unknown_datapoints = 29
> +rra[5].cdp_prep[16].value = -inf
> +rra[5].cdp_prep[16].unknown_datapoints = 29
> +rra[5].cdp_prep[17].value = -inf
> +rra[5].cdp_prep[17].unknown_datapoints = 29
> +rra[5].cdp_prep[18].value = -inf
> +rra[5].cdp_prep[18].unknown_datapoints = 29
> +rra[6].cf = "MAX"
> +rra[6].rows = 1440
> +rra[6].cur_row = 432
> +rra[6].pdp_per_row = 360
> +rra[6].xff = 5.0000000000e-01
> +rra[6].cdp_prep[0].value = -inf
> +rra[6].cdp_prep[0].unknown_datapoints = 239
> +rra[6].cdp_prep[1].value = -inf
> +rra[6].cdp_prep[1].unknown_datapoints = 239
> +rra[6].cdp_prep[2].value = -inf
> +rra[6].cdp_prep[2].unknown_datapoints = 239
> +rra[6].cdp_prep[3].value = -inf
> +rra[6].cdp_prep[3].unknown_datapoints = 239
> +rra[6].cdp_prep[4].value = -inf
> +rra[6].cdp_prep[4].unknown_datapoints = 239
> +rra[6].cdp_prep[5].value = -inf
> +rra[6].cdp_prep[5].unknown_datapoints = 239
> +rra[6].cdp_prep[6].value = -inf
> +rra[6].cdp_prep[6].unknown_datapoints = 239
> +rra[6].cdp_prep[7].value = -inf
> +rra[6].cdp_prep[7].unknown_datapoints = 239
> +rra[6].cdp_prep[8].value = -inf
> +rra[6].cdp_prep[8].unknown_datapoints = 239
> +rra[6].cdp_prep[9].value = -inf
> +rra[6].cdp_prep[9].unknown_datapoints = 239
> +rra[6].cdp_prep[10].value = -inf
> +rra[6].cdp_prep[10].unknown_datapoints = 239
> +rra[6].cdp_prep[11].value = -inf
> +rra[6].cdp_prep[11].unknown_datapoints = 239
> +rra[6].cdp_prep[12].value = -inf
> +rra[6].cdp_prep[12].unknown_datapoints = 239
> +rra[6].cdp_prep[13].value = -inf
> +rra[6].cdp_prep[13].unknown_datapoints = 239
> +rra[6].cdp_prep[14].value = -inf
> +rra[6].cdp_prep[14].unknown_datapoints = 239
> +rra[6].cdp_prep[15].value = -inf
> +rra[6].cdp_prep[15].unknown_datapoints = 239
> +rra[6].cdp_prep[16].value = -inf
> +rra[6].cdp_prep[16].unknown_datapoints = 239
> +rra[6].cdp_prep[17].value = -inf
> +rra[6].cdp_prep[17].unknown_datapoints = 239
> +rra[6].cdp_prep[18].value = -inf
> +rra[6].cdp_prep[18].unknown_datapoints = 239
> +rra[7].cf = "MAX"
> +rra[7].rows = 570
> +rra[7].cur_row = 400
> +rra[7].pdp_per_row = 10080
> +rra[7].xff = 5.0000000000e-01
> +rra[7].cdp_prep[0].value = -inf
> +rra[7].cdp_prep[0].unknown_datapoints = 1319
> +rra[7].cdp_prep[1].value = -inf
> +rra[7].cdp_prep[1].unknown_datapoints = 1319
> +rra[7].cdp_prep[2].value = -inf
> +rra[7].cdp_prep[2].unknown_datapoints = 1319
> +rra[7].cdp_prep[3].value = -inf
> +rra[7].cdp_prep[3].unknown_datapoints = 1319
> +rra[7].cdp_prep[4].value = -inf
> +rra[7].cdp_prep[4].unknown_datapoints = 1319
> +rra[7].cdp_prep[5].value = -inf
> +rra[7].cdp_prep[5].unknown_datapoints = 1319
> +rra[7].cdp_prep[6].value = -inf
> +rra[7].cdp_prep[6].unknown_datapoints = 1319
> +rra[7].cdp_prep[7].value = -inf
> +rra[7].cdp_prep[7].unknown_datapoints = 1319
> +rra[7].cdp_prep[8].value = -inf
> +rra[7].cdp_prep[8].unknown_datapoints = 1319
> +rra[7].cdp_prep[9].value = -inf
> +rra[7].cdp_prep[9].unknown_datapoints = 1319
> +rra[7].cdp_prep[10].value = -inf
> +rra[7].cdp_prep[10].unknown_datapoints = 1319
> +rra[7].cdp_prep[11].value = -inf
> +rra[7].cdp_prep[11].unknown_datapoints = 1319
> +rra[7].cdp_prep[12].value = -inf
> +rra[7].cdp_prep[12].unknown_datapoints = 1319
> +rra[7].cdp_prep[13].value = -inf
> +rra[7].cdp_prep[13].unknown_datapoints = 1319
> +rra[7].cdp_prep[14].value = -inf
> +rra[7].cdp_prep[14].unknown_datapoints = 1319
> +rra[7].cdp_prep[15].value = -inf
> +rra[7].cdp_prep[15].unknown_datapoints = 1319
> +rra[7].cdp_prep[16].value = -inf
> +rra[7].cdp_prep[16].unknown_datapoints = 1319
> +rra[7].cdp_prep[17].value = -inf
> +rra[7].cdp_prep[17].unknown_datapoints = 1319
> +rra[7].cdp_prep[18].value = -inf
> +rra[7].cdp_prep[18].unknown_datapoints = 1319
> diff --git a/tests/resources/compare/pve-storage-9.0_testnode_iso b/tests/resources/compare/pve-storage-9.0_testnode_iso
> new file mode 100644
> index 0000000..31d9bd8
> --- /dev/null
> +++ b/tests/resources/compare/pve-storage-9.0_testnode_iso
> @@ -0,0 +1,93 @@
> +filename = "tmp_tests/target/pve-storage-9.0/testnode/iso"
> +rrd_version = "0003"
> +step = 60
> +last_update = 1753999190
> +header_size = 2912
> +ds[total].index = 0
> +ds[total].type = "GAUGE"
> +ds[total].minimal_heartbeat = 120
> +ds[total].min = 0.0000000000e+00
> +ds[total].max = NaN
> +ds[total].last_ds = "U"
> +ds[total].value = NaN
> +ds[total].unknown_sec = 50
> +ds[used].index = 1
> +ds[used].type = "GAUGE"
> +ds[used].minimal_heartbeat = 120
> +ds[used].min = 0.0000000000e+00
> +ds[used].max = NaN
> +ds[used].last_ds = "U"
> +ds[used].value = NaN
> +ds[used].unknown_sec = 50
> +rra[0].cf = "AVERAGE"
> +rra[0].rows = 1440
> +rra[0].cur_row = 304
> +rra[0].pdp_per_row = 1
> +rra[0].xff = 5.0000000000e-01
> +rra[0].cdp_prep[0].value = NaN
> +rra[0].cdp_prep[0].unknown_datapoints = 0
> +rra[0].cdp_prep[1].value = NaN
> +rra[0].cdp_prep[1].unknown_datapoints = 0
> +rra[1].cf = "AVERAGE"
> +rra[1].rows = 1440
> +rra[1].cur_row = 1136
> +rra[1].pdp_per_row = 30
> +rra[1].xff = 5.0000000000e-01
> +rra[1].cdp_prep[0].value = 0.0000000000e+00
> +rra[1].cdp_prep[0].unknown_datapoints = 29
> +rra[1].cdp_prep[1].value = 0.0000000000e+00
> +rra[1].cdp_prep[1].unknown_datapoints = 29
> +rra[2].cf = "AVERAGE"
> +rra[2].rows = 1440
> +rra[2].cur_row = 438
> +rra[2].pdp_per_row = 360
> +rra[2].xff = 5.0000000000e-01
> +rra[2].cdp_prep[0].value = 0.0000000000e+00
> +rra[2].cdp_prep[0].unknown_datapoints = 239
> +rra[2].cdp_prep[1].value = 0.0000000000e+00
> +rra[2].cdp_prep[1].unknown_datapoints = 239
> +rra[3].cf = "AVERAGE"
> +rra[3].rows = 570
> +rra[3].cur_row = 430
> +rra[3].pdp_per_row = 10080
> +rra[3].xff = 5.0000000000e-01
> +rra[3].cdp_prep[0].value = 0.0000000000e+00
> +rra[3].cdp_prep[0].unknown_datapoints = 1319
> +rra[3].cdp_prep[1].value = 0.0000000000e+00
> +rra[3].cdp_prep[1].unknown_datapoints = 1319
> +rra[4].cf = "MAX"
> +rra[4].rows = 1440
> +rra[4].cur_row = 945
> +rra[4].pdp_per_row = 1
> +rra[4].xff = 5.0000000000e-01
> +rra[4].cdp_prep[0].value = NaN
> +rra[4].cdp_prep[0].unknown_datapoints = 0
> +rra[4].cdp_prep[1].value = NaN
> +rra[4].cdp_prep[1].unknown_datapoints = 0
> +rra[5].cf = "MAX"
> +rra[5].rows = 1440
> +rra[5].cur_row = 356
> +rra[5].pdp_per_row = 30
> +rra[5].xff = 5.0000000000e-01
> +rra[5].cdp_prep[0].value = -inf
> +rra[5].cdp_prep[0].unknown_datapoints = 29
> +rra[5].cdp_prep[1].value = -inf
> +rra[5].cdp_prep[1].unknown_datapoints = 29
> +rra[6].cf = "MAX"
> +rra[6].rows = 1440
> +rra[6].cur_row = 1349
> +rra[6].pdp_per_row = 360
> +rra[6].xff = 5.0000000000e-01
> +rra[6].cdp_prep[0].value = -inf
> +rra[6].cdp_prep[0].unknown_datapoints = 239
> +rra[6].cdp_prep[1].value = -inf
> +rra[6].cdp_prep[1].unknown_datapoints = 239
> +rra[7].cf = "MAX"
> +rra[7].rows = 570
> +rra[7].cur_row = 421
> +rra[7].pdp_per_row = 10080
> +rra[7].xff = 5.0000000000e-01
> +rra[7].cdp_prep[0].value = -inf
> +rra[7].cdp_prep[0].unknown_datapoints = 1319
> +rra[7].cdp_prep[1].value = -inf
> +rra[7].cdp_prep[1].unknown_datapoints = 1319
> diff --git a/tests/resources/compare/pve-vm-9.0_100 b/tests/resources/compare/pve-vm-9.0_100
> new file mode 100644
> index 0000000..9658dc4
> --- /dev/null
> +++ b/tests/resources/compare/pve-vm-9.0_100
> @@ -0,0 +1,453 @@
> +filename = "tmp_tests/target/pve-vm-9.0/100"
> +rrd_version = "0003"
> +step = 60
> +last_update = 1753999190
> +header_size = 15992
> +ds[maxcpu].index = 0
> +ds[maxcpu].type = "GAUGE"
> +ds[maxcpu].minimal_heartbeat = 120
> +ds[maxcpu].min = 0.0000000000e+00
> +ds[maxcpu].max = NaN
> +ds[maxcpu].last_ds = "U"
> +ds[maxcpu].value = NaN
> +ds[maxcpu].unknown_sec = 50
> +ds[cpu].index = 1
> +ds[cpu].type = "GAUGE"
> +ds[cpu].minimal_heartbeat = 120
> +ds[cpu].min = 0.0000000000e+00
> +ds[cpu].max = NaN
> +ds[cpu].last_ds = "U"
> +ds[cpu].value = NaN
> +ds[cpu].unknown_sec = 50
> +ds[maxmem].index = 2
> +ds[maxmem].type = "GAUGE"
> +ds[maxmem].minimal_heartbeat = 120
> +ds[maxmem].min = 0.0000000000e+00
> +ds[maxmem].max = NaN
> +ds[maxmem].last_ds = "U"
> +ds[maxmem].value = NaN
> +ds[maxmem].unknown_sec = 50
> +ds[mem].index = 3
> +ds[mem].type = "GAUGE"
> +ds[mem].minimal_heartbeat = 120
> +ds[mem].min = 0.0000000000e+00
> +ds[mem].max = NaN
> +ds[mem].last_ds = "U"
> +ds[mem].value = NaN
> +ds[mem].unknown_sec = 50
> +ds[maxdisk].index = 4
> +ds[maxdisk].type = "GAUGE"
> +ds[maxdisk].minimal_heartbeat = 120
> +ds[maxdisk].min = 0.0000000000e+00
> +ds[maxdisk].max = NaN
> +ds[maxdisk].last_ds = "U"
> +ds[maxdisk].value = NaN
> +ds[maxdisk].unknown_sec = 50
> +ds[disk].index = 5
> +ds[disk].type = "GAUGE"
> +ds[disk].minimal_heartbeat = 120
> +ds[disk].min = 0.0000000000e+00
> +ds[disk].max = NaN
> +ds[disk].last_ds = "U"
> +ds[disk].value = NaN
> +ds[disk].unknown_sec = 50
> +ds[netin].index = 6
> +ds[netin].type = "DERIVE"
> +ds[netin].minimal_heartbeat = 120
> +ds[netin].min = 0.0000000000e+00
> +ds[netin].max = NaN
> +ds[netin].last_ds = "U"
> +ds[netin].value = NaN
> +ds[netin].unknown_sec = 50
> +ds[netout].index = 7
> +ds[netout].type = "DERIVE"
> +ds[netout].minimal_heartbeat = 120
> +ds[netout].min = 0.0000000000e+00
> +ds[netout].max = NaN
> +ds[netout].last_ds = "U"
> +ds[netout].value = NaN
> +ds[netout].unknown_sec = 50
> +ds[diskread].index = 8
> +ds[diskread].type = "DERIVE"
> +ds[diskread].minimal_heartbeat = 120
> +ds[diskread].min = 0.0000000000e+00
> +ds[diskread].max = NaN
> +ds[diskread].last_ds = "U"
> +ds[diskread].value = NaN
> +ds[diskread].unknown_sec = 50
> +ds[diskwrite].index = 9
> +ds[diskwrite].type = "DERIVE"
> +ds[diskwrite].minimal_heartbeat = 120
> +ds[diskwrite].min = 0.0000000000e+00
> +ds[diskwrite].max = NaN
> +ds[diskwrite].last_ds = "U"
> +ds[diskwrite].value = NaN
> +ds[diskwrite].unknown_sec = 50
> +ds[memhost].index = 10
> +ds[memhost].type = "GAUGE"
> +ds[memhost].minimal_heartbeat = 120
> +ds[memhost].min = 0.0000000000e+00
> +ds[memhost].max = NaN
> +ds[memhost].last_ds = "U"
> +ds[memhost].value = NaN
> +ds[memhost].unknown_sec = 50
> +ds[pressurecpusome].index = 11
> +ds[pressurecpusome].type = "GAUGE"
> +ds[pressurecpusome].minimal_heartbeat = 120
> +ds[pressurecpusome].min = 0.0000000000e+00
> +ds[pressurecpusome].max = NaN
> +ds[pressurecpusome].last_ds = "U"
> +ds[pressurecpusome].value = NaN
> +ds[pressurecpusome].unknown_sec = 50
> +ds[pressurecpufull].index = 12
> +ds[pressurecpufull].type = "GAUGE"
> +ds[pressurecpufull].minimal_heartbeat = 120
> +ds[pressurecpufull].min = 0.0000000000e+00
> +ds[pressurecpufull].max = NaN
> +ds[pressurecpufull].last_ds = "U"
> +ds[pressurecpufull].value = NaN
> +ds[pressurecpufull].unknown_sec = 50
> +ds[pressureiosome].index = 13
> +ds[pressureiosome].type = "GAUGE"
> +ds[pressureiosome].minimal_heartbeat = 120
> +ds[pressureiosome].min = 0.0000000000e+00
> +ds[pressureiosome].max = NaN
> +ds[pressureiosome].last_ds = "U"
> +ds[pressureiosome].value = NaN
> +ds[pressureiosome].unknown_sec = 50
> +ds[pressureiofull].index = 14
> +ds[pressureiofull].type = "GAUGE"
> +ds[pressureiofull].minimal_heartbeat = 120
> +ds[pressureiofull].min = 0.0000000000e+00
> +ds[pressureiofull].max = NaN
> +ds[pressureiofull].last_ds = "U"
> +ds[pressureiofull].value = NaN
> +ds[pressureiofull].unknown_sec = 50
> +ds[pressurememorysome].index = 15
> +ds[pressurememorysome].type = "GAUGE"
> +ds[pressurememorysome].minimal_heartbeat = 120
> +ds[pressurememorysome].min = 0.0000000000e+00
> +ds[pressurememorysome].max = NaN
> +ds[pressurememorysome].last_ds = "U"
> +ds[pressurememorysome].value = NaN
> +ds[pressurememorysome].unknown_sec = 50
> +ds[pressurememoryfull].index = 16
> +ds[pressurememoryfull].type = "GAUGE"
> +ds[pressurememoryfull].minimal_heartbeat = 120
> +ds[pressurememoryfull].min = 0.0000000000e+00
> +ds[pressurememoryfull].max = NaN
> +ds[pressurememoryfull].last_ds = "U"
> +ds[pressurememoryfull].value = NaN
> +ds[pressurememoryfull].unknown_sec = 50
> +rra[0].cf = "AVERAGE"
> +rra[0].rows = 1440
> +rra[0].cur_row = 1099
> +rra[0].pdp_per_row = 1
> +rra[0].xff = 5.0000000000e-01
> +rra[0].cdp_prep[0].value = NaN
> +rra[0].cdp_prep[0].unknown_datapoints = 0
> +rra[0].cdp_prep[1].value = NaN
> +rra[0].cdp_prep[1].unknown_datapoints = 0
> +rra[0].cdp_prep[2].value = NaN
> +rra[0].cdp_prep[2].unknown_datapoints = 0
> +rra[0].cdp_prep[3].value = NaN
> +rra[0].cdp_prep[3].unknown_datapoints = 0
> +rra[0].cdp_prep[4].value = NaN
> +rra[0].cdp_prep[4].unknown_datapoints = 0
> +rra[0].cdp_prep[5].value = NaN
> +rra[0].cdp_prep[5].unknown_datapoints = 0
> +rra[0].cdp_prep[6].value = NaN
> +rra[0].cdp_prep[6].unknown_datapoints = 0
> +rra[0].cdp_prep[7].value = NaN
> +rra[0].cdp_prep[7].unknown_datapoints = 0
> +rra[0].cdp_prep[8].value = NaN
> +rra[0].cdp_prep[8].unknown_datapoints = 0
> +rra[0].cdp_prep[9].value = NaN
> +rra[0].cdp_prep[9].unknown_datapoints = 0
> +rra[0].cdp_prep[10].value = NaN
> +rra[0].cdp_prep[10].unknown_datapoints = 0
> +rra[0].cdp_prep[11].value = NaN
> +rra[0].cdp_prep[11].unknown_datapoints = 0
> +rra[0].cdp_prep[12].value = NaN
> +rra[0].cdp_prep[12].unknown_datapoints = 0
> +rra[0].cdp_prep[13].value = NaN
> +rra[0].cdp_prep[13].unknown_datapoints = 0
> +rra[0].cdp_prep[14].value = NaN
> +rra[0].cdp_prep[14].unknown_datapoints = 0
> +rra[0].cdp_prep[15].value = NaN
> +rra[0].cdp_prep[15].unknown_datapoints = 0
> +rra[0].cdp_prep[16].value = NaN
> +rra[0].cdp_prep[16].unknown_datapoints = 0
> +rra[1].cf = "AVERAGE"
> +rra[1].rows = 1440
> +rra[1].cur_row = 551
> +rra[1].pdp_per_row = 30
> +rra[1].xff = 5.0000000000e-01
> +rra[1].cdp_prep[0].value = 0.0000000000e+00
> +rra[1].cdp_prep[0].unknown_datapoints = 29
> +rra[1].cdp_prep[1].value = 0.0000000000e+00
> +rra[1].cdp_prep[1].unknown_datapoints = 29
> +rra[1].cdp_prep[2].value = 0.0000000000e+00
> +rra[1].cdp_prep[2].unknown_datapoints = 29
> +rra[1].cdp_prep[3].value = 0.0000000000e+00
> +rra[1].cdp_prep[3].unknown_datapoints = 29
> +rra[1].cdp_prep[4].value = 0.0000000000e+00
> +rra[1].cdp_prep[4].unknown_datapoints = 29
> +rra[1].cdp_prep[5].value = 0.0000000000e+00
> +rra[1].cdp_prep[5].unknown_datapoints = 29
> +rra[1].cdp_prep[6].value = 0.0000000000e+00
> +rra[1].cdp_prep[6].unknown_datapoints = 29
> +rra[1].cdp_prep[7].value = 0.0000000000e+00
> +rra[1].cdp_prep[7].unknown_datapoints = 29
> +rra[1].cdp_prep[8].value = 0.0000000000e+00
> +rra[1].cdp_prep[8].unknown_datapoints = 29
> +rra[1].cdp_prep[9].value = 0.0000000000e+00
> +rra[1].cdp_prep[9].unknown_datapoints = 29
> +rra[1].cdp_prep[10].value = 0.0000000000e+00
> +rra[1].cdp_prep[10].unknown_datapoints = 29
> +rra[1].cdp_prep[11].value = 0.0000000000e+00
> +rra[1].cdp_prep[11].unknown_datapoints = 29
> +rra[1].cdp_prep[12].value = 0.0000000000e+00
> +rra[1].cdp_prep[12].unknown_datapoints = 29
> +rra[1].cdp_prep[13].value = 0.0000000000e+00
> +rra[1].cdp_prep[13].unknown_datapoints = 29
> +rra[1].cdp_prep[14].value = 0.0000000000e+00
> +rra[1].cdp_prep[14].unknown_datapoints = 29
> +rra[1].cdp_prep[15].value = 0.0000000000e+00
> +rra[1].cdp_prep[15].unknown_datapoints = 29
> +rra[1].cdp_prep[16].value = 0.0000000000e+00
> +rra[1].cdp_prep[16].unknown_datapoints = 29
> +rra[2].cf = "AVERAGE"
> +rra[2].rows = 1440
> +rra[2].cur_row = 1387
> +rra[2].pdp_per_row = 360
> +rra[2].xff = 5.0000000000e-01
> +rra[2].cdp_prep[0].value = 0.0000000000e+00
> +rra[2].cdp_prep[0].unknown_datapoints = 239
> +rra[2].cdp_prep[1].value = 0.0000000000e+00
> +rra[2].cdp_prep[1].unknown_datapoints = 239
> +rra[2].cdp_prep[2].value = 0.0000000000e+00
> +rra[2].cdp_prep[2].unknown_datapoints = 239
> +rra[2].cdp_prep[3].value = 0.0000000000e+00
> +rra[2].cdp_prep[3].unknown_datapoints = 239
> +rra[2].cdp_prep[4].value = 0.0000000000e+00
> +rra[2].cdp_prep[4].unknown_datapoints = 239
> +rra[2].cdp_prep[5].value = 0.0000000000e+00
> +rra[2].cdp_prep[5].unknown_datapoints = 239
> +rra[2].cdp_prep[6].value = 0.0000000000e+00
> +rra[2].cdp_prep[6].unknown_datapoints = 239
> +rra[2].cdp_prep[7].value = 0.0000000000e+00
> +rra[2].cdp_prep[7].unknown_datapoints = 239
> +rra[2].cdp_prep[8].value = 0.0000000000e+00
> +rra[2].cdp_prep[8].unknown_datapoints = 239
> +rra[2].cdp_prep[9].value = 0.0000000000e+00
> +rra[2].cdp_prep[9].unknown_datapoints = 239
> +rra[2].cdp_prep[10].value = 0.0000000000e+00
> +rra[2].cdp_prep[10].unknown_datapoints = 239
> +rra[2].cdp_prep[11].value = 0.0000000000e+00
> +rra[2].cdp_prep[11].unknown_datapoints = 239
> +rra[2].cdp_prep[12].value = 0.0000000000e+00
> +rra[2].cdp_prep[12].unknown_datapoints = 239
> +rra[2].cdp_prep[13].value = 0.0000000000e+00
> +rra[2].cdp_prep[13].unknown_datapoints = 239
> +rra[2].cdp_prep[14].value = 0.0000000000e+00
> +rra[2].cdp_prep[14].unknown_datapoints = 239
> +rra[2].cdp_prep[15].value = 0.0000000000e+00
> +rra[2].cdp_prep[15].unknown_datapoints = 239
> +rra[2].cdp_prep[16].value = 0.0000000000e+00
> +rra[2].cdp_prep[16].unknown_datapoints = 239
> +rra[3].cf = "AVERAGE"
> +rra[3].rows = 570
> +rra[3].cur_row = 100
> +rra[3].pdp_per_row = 10080
> +rra[3].xff = 5.0000000000e-01
> +rra[3].cdp_prep[0].value = 0.0000000000e+00
> +rra[3].cdp_prep[0].unknown_datapoints = 1319
> +rra[3].cdp_prep[1].value = 0.0000000000e+00
> +rra[3].cdp_prep[1].unknown_datapoints = 1319
> +rra[3].cdp_prep[2].value = 0.0000000000e+00
> +rra[3].cdp_prep[2].unknown_datapoints = 1319
> +rra[3].cdp_prep[3].value = 0.0000000000e+00
> +rra[3].cdp_prep[3].unknown_datapoints = 1319
> +rra[3].cdp_prep[4].value = 0.0000000000e+00
> +rra[3].cdp_prep[4].unknown_datapoints = 1319
> +rra[3].cdp_prep[5].value = 0.0000000000e+00
> +rra[3].cdp_prep[5].unknown_datapoints = 1319
> +rra[3].cdp_prep[6].value = 0.0000000000e+00
> +rra[3].cdp_prep[6].unknown_datapoints = 1319
> +rra[3].cdp_prep[7].value = 0.0000000000e+00
> +rra[3].cdp_prep[7].unknown_datapoints = 1319
> +rra[3].cdp_prep[8].value = 0.0000000000e+00
> +rra[3].cdp_prep[8].unknown_datapoints = 1319
> +rra[3].cdp_prep[9].value = 0.0000000000e+00
> +rra[3].cdp_prep[9].unknown_datapoints = 1319
> +rra[3].cdp_prep[10].value = 0.0000000000e+00
> +rra[3].cdp_prep[10].unknown_datapoints = 1319
> +rra[3].cdp_prep[11].value = 0.0000000000e+00
> +rra[3].cdp_prep[11].unknown_datapoints = 1319
> +rra[3].cdp_prep[12].value = 0.0000000000e+00
> +rra[3].cdp_prep[12].unknown_datapoints = 1319
> +rra[3].cdp_prep[13].value = 0.0000000000e+00
> +rra[3].cdp_prep[13].unknown_datapoints = 1319
> +rra[3].cdp_prep[14].value = 0.0000000000e+00
> +rra[3].cdp_prep[14].unknown_datapoints = 1319
> +rra[3].cdp_prep[15].value = 0.0000000000e+00
> +rra[3].cdp_prep[15].unknown_datapoints = 1319
> +rra[3].cdp_prep[16].value = 0.0000000000e+00
> +rra[3].cdp_prep[16].unknown_datapoints = 1319
> +rra[4].cf = "MAX"
> +rra[4].rows = 1440
> +rra[4].cur_row = 216
> +rra[4].pdp_per_row = 1
> +rra[4].xff = 5.0000000000e-01
> +rra[4].cdp_prep[0].value = NaN
> +rra[4].cdp_prep[0].unknown_datapoints = 0
> +rra[4].cdp_prep[1].value = NaN
> +rra[4].cdp_prep[1].unknown_datapoints = 0
> +rra[4].cdp_prep[2].value = NaN
> +rra[4].cdp_prep[2].unknown_datapoints = 0
> +rra[4].cdp_prep[3].value = NaN
> +rra[4].cdp_prep[3].unknown_datapoints = 0
> +rra[4].cdp_prep[4].value = NaN
> +rra[4].cdp_prep[4].unknown_datapoints = 0
> +rra[4].cdp_prep[5].value = NaN
> +rra[4].cdp_prep[5].unknown_datapoints = 0
> +rra[4].cdp_prep[6].value = NaN
> +rra[4].cdp_prep[6].unknown_datapoints = 0
> +rra[4].cdp_prep[7].value = NaN
> +rra[4].cdp_prep[7].unknown_datapoints = 0
> +rra[4].cdp_prep[8].value = NaN
> +rra[4].cdp_prep[8].unknown_datapoints = 0
> +rra[4].cdp_prep[9].value = NaN
> +rra[4].cdp_prep[9].unknown_datapoints = 0
> +rra[4].cdp_prep[10].value = NaN
> +rra[4].cdp_prep[10].unknown_datapoints = 0
> +rra[4].cdp_prep[11].value = NaN
> +rra[4].cdp_prep[11].unknown_datapoints = 0
> +rra[4].cdp_prep[12].value = NaN
> +rra[4].cdp_prep[12].unknown_datapoints = 0
> +rra[4].cdp_prep[13].value = NaN
> +rra[4].cdp_prep[13].unknown_datapoints = 0
> +rra[4].cdp_prep[14].value = NaN
> +rra[4].cdp_prep[14].unknown_datapoints = 0
> +rra[4].cdp_prep[15].value = NaN
> +rra[4].cdp_prep[15].unknown_datapoints = 0
> +rra[4].cdp_prep[16].value = NaN
> +rra[4].cdp_prep[16].unknown_datapoints = 0
> +rra[5].cf = "MAX"
> +rra[5].rows = 1440
> +rra[5].cur_row = 327
> +rra[5].pdp_per_row = 30
> +rra[5].xff = 5.0000000000e-01
> +rra[5].cdp_prep[0].value = -inf
> +rra[5].cdp_prep[0].unknown_datapoints = 29
> +rra[5].cdp_prep[1].value = -inf
> +rra[5].cdp_prep[1].unknown_datapoints = 29
> +rra[5].cdp_prep[2].value = -inf
> +rra[5].cdp_prep[2].unknown_datapoints = 29
> +rra[5].cdp_prep[3].value = -inf
> +rra[5].cdp_prep[3].unknown_datapoints = 29
> +rra[5].cdp_prep[4].value = -inf
> +rra[5].cdp_prep[4].unknown_datapoints = 29
> +rra[5].cdp_prep[5].value = -inf
> +rra[5].cdp_prep[5].unknown_datapoints = 29
> +rra[5].cdp_prep[6].value = -inf
> +rra[5].cdp_prep[6].unknown_datapoints = 29
> +rra[5].cdp_prep[7].value = -inf
> +rra[5].cdp_prep[7].unknown_datapoints = 29
> +rra[5].cdp_prep[8].value = -inf
> +rra[5].cdp_prep[8].unknown_datapoints = 29
> +rra[5].cdp_prep[9].value = -inf
> +rra[5].cdp_prep[9].unknown_datapoints = 29
> +rra[5].cdp_prep[10].value = -inf
> +rra[5].cdp_prep[10].unknown_datapoints = 29
> +rra[5].cdp_prep[11].value = -inf
> +rra[5].cdp_prep[11].unknown_datapoints = 29
> +rra[5].cdp_prep[12].value = -inf
> +rra[5].cdp_prep[12].unknown_datapoints = 29
> +rra[5].cdp_prep[13].value = -inf
> +rra[5].cdp_prep[13].unknown_datapoints = 29
> +rra[5].cdp_prep[14].value = -inf
> +rra[5].cdp_prep[14].unknown_datapoints = 29
> +rra[5].cdp_prep[15].value = -inf
> +rra[5].cdp_prep[15].unknown_datapoints = 29
> +rra[5].cdp_prep[16].value = -inf
> +rra[5].cdp_prep[16].unknown_datapoints = 29
> +rra[6].cf = "MAX"
> +rra[6].rows = 1440
> +rra[6].cur_row = 993
> +rra[6].pdp_per_row = 360
> +rra[6].xff = 5.0000000000e-01
> +rra[6].cdp_prep[0].value = -inf
> +rra[6].cdp_prep[0].unknown_datapoints = 239
> +rra[6].cdp_prep[1].value = -inf
> +rra[6].cdp_prep[1].unknown_datapoints = 239
> +rra[6].cdp_prep[2].value = -inf
> +rra[6].cdp_prep[2].unknown_datapoints = 239
> +rra[6].cdp_prep[3].value = -inf
> +rra[6].cdp_prep[3].unknown_datapoints = 239
> +rra[6].cdp_prep[4].value = -inf
> +rra[6].cdp_prep[4].unknown_datapoints = 239
> +rra[6].cdp_prep[5].value = -inf
> +rra[6].cdp_prep[5].unknown_datapoints = 239
> +rra[6].cdp_prep[6].value = -inf
> +rra[6].cdp_prep[6].unknown_datapoints = 239
> +rra[6].cdp_prep[7].value = -inf
> +rra[6].cdp_prep[7].unknown_datapoints = 239
> +rra[6].cdp_prep[8].value = -inf
> +rra[6].cdp_prep[8].unknown_datapoints = 239
> +rra[6].cdp_prep[9].value = -inf
> +rra[6].cdp_prep[9].unknown_datapoints = 239
> +rra[6].cdp_prep[10].value = -inf
> +rra[6].cdp_prep[10].unknown_datapoints = 239
> +rra[6].cdp_prep[11].value = -inf
> +rra[6].cdp_prep[11].unknown_datapoints = 239
> +rra[6].cdp_prep[12].value = -inf
> +rra[6].cdp_prep[12].unknown_datapoints = 239
> +rra[6].cdp_prep[13].value = -inf
> +rra[6].cdp_prep[13].unknown_datapoints = 239
> +rra[6].cdp_prep[14].value = -inf
> +rra[6].cdp_prep[14].unknown_datapoints = 239
> +rra[6].cdp_prep[15].value = -inf
> +rra[6].cdp_prep[15].unknown_datapoints = 239
> +rra[6].cdp_prep[16].value = -inf
> +rra[6].cdp_prep[16].unknown_datapoints = 239
> +rra[7].cf = "MAX"
> +rra[7].rows = 570
> +rra[7].cur_row = 165
> +rra[7].pdp_per_row = 10080
> +rra[7].xff = 5.0000000000e-01
> +rra[7].cdp_prep[0].value = -inf
> +rra[7].cdp_prep[0].unknown_datapoints = 1319
> +rra[7].cdp_prep[1].value = -inf
> +rra[7].cdp_prep[1].unknown_datapoints = 1319
> +rra[7].cdp_prep[2].value = -inf
> +rra[7].cdp_prep[2].unknown_datapoints = 1319
> +rra[7].cdp_prep[3].value = -inf
> +rra[7].cdp_prep[3].unknown_datapoints = 1319
> +rra[7].cdp_prep[4].value = -inf
> +rra[7].cdp_prep[4].unknown_datapoints = 1319
> +rra[7].cdp_prep[5].value = -inf
> +rra[7].cdp_prep[5].unknown_datapoints = 1319
> +rra[7].cdp_prep[6].value = -inf
> +rra[7].cdp_prep[6].unknown_datapoints = 1319
> +rra[7].cdp_prep[7].value = -inf
> +rra[7].cdp_prep[7].unknown_datapoints = 1319
> +rra[7].cdp_prep[8].value = -inf
> +rra[7].cdp_prep[8].unknown_datapoints = 1319
> +rra[7].cdp_prep[9].value = -inf
> +rra[7].cdp_prep[9].unknown_datapoints = 1319
> +rra[7].cdp_prep[10].value = -inf
> +rra[7].cdp_prep[10].unknown_datapoints = 1319
> +rra[7].cdp_prep[11].value = -inf
> +rra[7].cdp_prep[11].unknown_datapoints = 1319
> +rra[7].cdp_prep[12].value = -inf
> +rra[7].cdp_prep[12].unknown_datapoints = 1319
> +rra[7].cdp_prep[13].value = -inf
> +rra[7].cdp_prep[13].unknown_datapoints = 1319
> +rra[7].cdp_prep[14].value = -inf
> +rra[7].cdp_prep[14].unknown_datapoints = 1319
> +rra[7].cdp_prep[15].value = -inf
> +rra[7].cdp_prep[15].unknown_datapoints = 1319
> +rra[7].cdp_prep[16].value = -inf
> +rra[7].cdp_prep[16].unknown_datapoints = 1319
> diff --git a/tests/resources/compare/second_empty_run b/tests/resources/compare/second_empty_run
> new file mode 100644
> index 0000000..dc1e7f4
> --- /dev/null
> +++ b/tests/resources/compare/second_empty_run
> @@ -0,0 +1,8 @@
> +Migrating RRD data for nodes…
> +Migrated all nodes
> +Migrating RRD data for storages…
> +Migrated all storages
> +Migrating RRD data for guests…
> +Using 2 thread(s)
> +Migrated 0 guests
> +It took 0.00s
> diff --git a/tests/resources/compare/second_run_with_missed b/tests/resources/compare/second_run_with_missed
> new file mode 100644
> index 0000000..e1c7f71
> --- /dev/null
> +++ b/tests/resources/compare/second_run_with_missed
> @@ -0,0 +1,7 @@
> +Migrating RRD data for nodes…
> +Migrated all nodes
> +Migrating RRD data for storages…
> +Migrated all storages
> +Migrating RRD data for guests…
> +Using 2 thread(s)
> +Migrated 1 guests
> diff --git a/tests/resources/resourcelists/.members b/tests/resources/resourcelists/.members
> new file mode 100644
> index 0000000..2823203
> --- /dev/null
> +++ b/tests/resources/resourcelists/.members
> @@ -0,0 +1,10 @@
> +{
> +"nodename": "testnode",
> +"version": 5,
> +"cluster": { "name": "rrd-test", "version": 3, "nodes": 3, "quorate": 1 },
> +"nodelist": {
> +  "testnode": { "id": 1, "online": 1, "ip": "10.9.9.47"},
> +  "othernode": { "id": 2, "online": 1, "ip": "10.9.9.48"},
> +  "thirdnode": { "id": 3, "online": 1, "ip": "10.9.9.49"}
> +  }
> +}
> diff --git a/tests/resources/resourcelists/.vmlist b/tests/resources/resourcelists/.vmlist
> new file mode 100644
> index 0000000..d367140
> --- /dev/null
> +++ b/tests/resources/resourcelists/.vmlist
> @@ -0,0 +1,7 @@
> +{
> +"version": 7,
> +"ids": {
> +"100": { "node": "testnode", "type": "qemu", "version": 61 },
> +"101": { "node": "testnode", "type": "qemu", "version": 61 },
> +
> +}
> diff --git a/tests/resources/source/pve2-node/othernode.old b/tests/resources/source/pve2-node/othernode.old
> new file mode 100644
> index 0000000000000000000000000000000000000000..9da2327e267563690d2b69a05b976c1870e91836
> GIT binary patch
> literal 81008
> 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
>
> literal 0
> HcmV?d00001
>
> diff --git a/tests/resources/source/pve2-node/testnode b/tests/resources/source/pve2-node/testnode
> new file mode 100644
> index 0000000000000000000000000000000000000000..9da2327e267563690d2b69a05b976c1870e91836
> GIT binary patch
> literal 81008
> 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
>
> literal 0
> HcmV?d00001
>
> diff --git a/tests/resources/source/pve2-storage/testnode/foo.old b/tests/resources/source/pve2-storage/testnode/foo.old
> new file mode 100644
> index 0000000000000000000000000000000000000000..1f37f59f79b2a5c75eb9f02a24f093bc6c84947b
> GIT binary patch
> literal 14688
> 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
>
> literal 0
> HcmV?d00001
>
> diff --git a/tests/resources/source/pve2-storage/testnode/iso b/tests/resources/source/pve2-storage/testnode/iso
> new file mode 100644
> index 0000000000000000000000000000000000000000..1f37f59f79b2a5c75eb9f02a24f093bc6c84947b
> GIT binary patch
> literal 14688
> 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
>
> literal 0
> HcmV?d00001
>
> diff --git a/tests/resources/source/pve2-vm/100 b/tests/resources/source/pve2-vm/100
> new file mode 100644
> index 0000000000000000000000000000000000000000..a0c8f286362dfbe63961ed057cf47ed7aa6a78f4
> GIT binary patch
> literal 67744
> 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
> E1Fg*h5&!@I
>
> literal 0
> HcmV?d00001
>
> diff --git a/tests/resources/source/pve2-vm/400 b/tests/resources/source/pve2-vm/400
> new file mode 100644
> index 0000000000000000000000000000000000000000..a0c8f286362dfbe63961ed057cf47ed7aa6a78f4
> GIT binary patch
> literal 67744
> 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
> E1Fg*h5&!@I
>
> literal 0
> HcmV?d00001
>
> diff --git a/tests/resources/source/pve2-vm/500.old b/tests/resources/source/pve2-vm/500.old
> new file mode 100644
> index 0000000000000000000000000000000000000000..a0c8f286362dfbe63961ed057cf47ed7aa6a78f4
> GIT binary patch
> literal 67744
> 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
> E1Fg*h5&!@I
>
> literal 0
> HcmV?d00001
>
> diff --git a/tests/utils.rs b/tests/utils.rs
> new file mode 100644
> index 0000000..be56095
> --- /dev/null
> +++ b/tests/utils.rs
> @@ -0,0 +1,117 @@
> +use pretty_assertions::assert_eq;
> +use std::{
> +    env, fs,
> +    io::{BufRead, Cursor},
> +    path::{Path, PathBuf},
> +    process::Command,
> +};
> +
> +pub const TMPDIR: &str = "tmp_tests";
> +pub const TMPDIR_SOURCE_BASEDIR: &str = "tmp_tests/resources/source";
> +pub const TMPDIR_TARGET: &str = "tmp_tests/target";
> +pub const TMPDIR_COMPARE: &str = "tmp_tests/resources/compare";
> +pub const TMPDIR_RESOURCELISTS: &str = "tmp_tests/resources/resourcelists";
> +pub const TEST_RESOURCE_DIR: &str = "tests/resources";
> +
> +fn get_target_dir() -> PathBuf {
> +    let bin = env::current_exe().expect("exe path");
> +    let mut target_dir = PathBuf::from(bin.parent().expect("bin parent"));
> +    target_dir.pop();
> +    target_dir
> +}
> +
> +pub fn migration_tool_path() -> String {
> +    let mut target_dir = get_target_dir();
> +    target_dir.push("proxmox-rrd-migration-tool");
> +    target_dir.to_str().unwrap().to_string()
> +}
> +
> +/// Prepare the directory with the source files on which the tests are performed
> +pub fn test_prepare() {
> +    let tmpdir = Path::new(TMPDIR);
> +
> +    println!("Setting up test tmp dir");
> +    if tmpdir.exists() {
> +        fs::remove_dir_all(tmpdir).expect("remove tmpdir");
> +    }
> +    fs::create_dir(tmpdir).expect("create tmpdir");
> +    fs::create_dir_all(TMPDIR_TARGET).expect("created tmp target dir");
> +
> +    Command::new("cp")
> +        .args(["-ra", TEST_RESOURCE_DIR, TMPDIR])
> +        .output()
> +        .expect("copy test resource files");
> +}
> +
> +/// Loop over directories to compare results
> +///
> +/// type:               type of test, node, guest, storage
> +/// target_path:        path to the dir where the target RRD files are
> +/// comp_subdir_prefix: subdir prefix where the target files are expetect to be per type
> +pub fn compare_results(migrationtype: &str, target_path: &PathBuf, comp_subdir_prefix: &str) {
> +    fs::read_dir(&target_path)
> +        .expect(format!("could not read target {migrationtype} dir").as_str())
> +        .filter(|f| f.is_ok())
> +        .map(|f| f.unwrap().path())
> +        .filter(|f| f.is_file())

You can use filter_map here

> +        .for_each(|file| {
> +            let path = file.as_path();
> +
> +            let expected_path: PathBuf = [
> +                TMPDIR_COMPARE,
> +                format!(
> +                    "{}_{}",
> +                    comp_subdir_prefix,
> +                    file.file_name().unwrap().to_string_lossy()
> +                )
> +                .as_str(),
> +            ]
> +            .iter()
> +            .collect();
> +            let expected = fs::read_to_string(expected_path).expect("read compare file");
> +            let testcase = String::from_utf8(
> +                Command::new("/usr/bin/rrdtool")
> +                    .args(["info", path.to_str().unwrap()])
> +                    .output()
> +                    .expect("execute rrdtool info")
> +                    .stdout,
> +            )
> +            .expect("rrdtool into to string");
> +            compare_rrdinfo_output(testcase, expected);
> +        });
> +}
> +
> +/// Compares the output of rrdinfo with the expected output.
> +pub fn compare_rrdinfo_output(testcase: String, expected: String) {
> +    let expected_lines: Vec<String> = expected.lines().map(|l| String::from(l)).collect();
> +    let testcase_lines: Vec<String> = testcase.lines().map(|l| String::from(l)).collect();
> +    assert_eq!(
> +        expected_lines.len(),
> +        testcase_lines.len(),
> +        "expected: {}, testcase: {}",
> +        expected_lines.len(),
> +        testcase_lines.len()
> +    );
> +    for (expected, command) in expected_lines.iter().zip(testcase_lines.iter()) {
> +        if expected.contains("cur_row") || expected.contains("last_update") {
> +            // these lines can still have different values regarding timing and ptr positions
> +            continue;
> +        }
> +        assert_eq!(expected, command);
> +    }
> +}
> +
> +/// Reads file and resturns it as a string, except for the last line
> +pub fn drop_last_line(content: Vec<u8>) -> String {
> +    let mut out: Vec<String> = Vec::new();
> +    let c = Cursor::new(content);
> +    let mut lines = c.lines();
> +    while let Some(line) = lines.next() {
> +        let line = line.expect("output line");
> +        out.push(line);
> +    }
> +    let _last_line = out.pop();
> +    let mut output = out.join("\n");
> +    output.push_str("\n");
> +    output
> +}



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH manager stabe-8+master v4 15/15] pve8to9: add checkfs for RRD migration
  2025-07-26  1:06 ` [pve-devel] [PATCH manager stabe-8+master v4 15/15] pve8to9: add checkfs for RRD migration Aaron Lauterer
@ 2025-07-29  8:15   ` Lukas Wagner
  2025-07-29  9:16     ` Thomas Lamprecht
  0 siblings, 1 reply; 50+ messages in thread
From: Lukas Wagner @ 2025-07-29  8:15 UTC (permalink / raw)
  To: Proxmox VE development discussion, Aaron Lauterer

On Sat Jul 26, 2025 at 3:06 AM CEST, Aaron Lauterer wrote:
> +sub check_rrd_migration {
> +    if (-e "/var/lib/rrdcached/db/pve-node-9.0") {
> +        log_info("Check post RRD migration situation...");
> +
> +        my $count = 0;
> +        my $count_occurences = sub {
> +            $count++;
> +        };
> +        eval {
> +            run_command(
> +                ['find /var/lib/rrdcached/db -type f ! -name "*.foo"'],

Shouldn't this be "*.old"?

> +                outfunc => $count_occurences,
> +                noerr => 1,
> +            );
> +        };
> +
> +        if ($count) {
> +            log_warn("Found '$count' RRD files that have not yet been migrated to the new schema."
> +                . " Please run the following command manually:\n"
> +                . "proxmox-rrd-migration-tool --migrate\n");
> +        }
> +
> +    } else {
> +        log_info("Check space requirements for RRD migration...");
> +        # multiplier values taken from KiB sizes of old and new RRD files
> +        my $rrd_dirs = {
> +            nodes => {
> +                path => "/var/lib/rrdcached/db/pve2-node",
> +                multiplier => 18.1,
> +            },
> +            guests => {
> +                path => "/var/lib/rrdcached/db/pve2-vm",
> +                multiplier => 20.2,
> +            },
> +            storage => {
> +                path => "/var/lib/rrdcached/db/pve2-storage",
> +                multiplier => 11.14,
> +            },
> +        };
> +
> +        my $size_buffer = 1024 * 1024 * 1024; # at least one GiB of free space should be calculated in

Could be worth defining some GIGABYTE constant...

> +        my $total_size_estimate = 0;
> +        for my $type (keys %$rrd_dirs) {
> +            my $size = PVE::Tools::du($rrd_dirs->{$type}->{path});
> +            $total_size_estimate =
> +                $total_size_estimate + ($size * $rrd_dirs->{$type}->{multiplier});
> +        }
> +        my $root_free = PVE::Tools::df('/', 10);
> +
> +        if (($total_size_estimate + $size_buffer) >= $root_free->{avail}) {
> +            my $estimate_gib = sprintf("%.2f", $total_size_estimate / 1024 / 1024 / 1024);
> +            my $free_gib = sprintf("%.2f", $root_free->{avail} / 1024 / 1024 / 1024);

...that you can also use here as a divider.

> +            log_fail("Not enough free space to migrate existing RRD files to the new format!\n"
> +                . "Migrating the current RRD files is expected to consume about ${estimate_gib} GiB plus 1 GiB of safety."
> +                . " But there is currently only ${free_gib} GiB space on the root file system available.\n"
> +            );
> +        }
> +    }
> +}
> +
>  sub check_virtual_guests {
>      print_header("VIRTUAL GUEST CHECKS");
>  
> @@ -1882,6 +1943,7 @@ sub check_misc {
>      check_legacy_notification_sections();
>      check_legacy_backup_job_options();
>      check_lvm_autoactivation();
> +    check_rrd_migration();
>  }
>  
>  my sub colored_if {



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH manager stabe-8+master v4 15/15] pve8to9: add checkfs for RRD migration
  2025-07-29  8:15   ` Lukas Wagner
@ 2025-07-29  9:16     ` Thomas Lamprecht
  0 siblings, 0 replies; 50+ messages in thread
From: Thomas Lamprecht @ 2025-07-29  9:16 UTC (permalink / raw)
  To: Proxmox VE development discussion, Lukas Wagner, Aaron Lauterer

Am 29.07.25 um 10:16 schrieb Lukas Wagner:
>> +            run_command(
>> +                ['find /var/lib/rrdcached/db -type f ! -name "*.foo"'],
> Shouldn't this be "*.old"?

I'd also think so, and it really should use a safer calling style, i.e.
actually pass the different arguments as separate arguments here already:

['find', '/var/lib/rrdcached/db', '-type', 'f', '!', '-name', '*.old'],




_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH proxmox-rrd-migration-tool v4 3/3] add debian packaging
  2025-07-28 14:36   ` Lukas Wagner
@ 2025-07-29  9:29     ` Thomas Lamprecht
  2025-07-29  9:49       ` Lukas Wagner
  0 siblings, 1 reply; 50+ messages in thread
From: Thomas Lamprecht @ 2025-07-29  9:29 UTC (permalink / raw)
  To: Proxmox VE development discussion, Lukas Wagner

Am 28.07.25 um 16:36 schrieb Lukas Wagner:
>> diff --git a/debian/links b/debian/links
>> new file mode 100644
>> index 0000000..9e59b57
>> --- /dev/null
>> +++ b/debian/links
>> @@ -0,0 +1 @@
>> +usr/libexec/proxmox/proxmox-rrd-migration-tool usr/bin/proxmox-rrd-migration-tool
> As far as I understand this is a tool that should be called directly by
> a user, right? In that case I'd not add the symlink but alter the
> Makefile so that it installs it to /usr/bin/ right away.

It actually shouldn't, the migration should be handled automatically and
the tool only called manually in the edge case, in which providing the full
path (e.g., in the respective 8to9 check output) is enough.

So while you're definitively right in that this way is not gaining us
anything (good catch!), I'd rather just drop the symnlink in /usr/bin and
keep it only in libexec.


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH cluster v4 1/2] status: introduce new pve-{type}- rrd and metric format
  2025-07-26  1:05 ` [pve-devel] [PATCH cluster v4 1/2] status: introduce new pve-{type}- rrd and metric format Aaron Lauterer
@ 2025-07-29  9:44   ` Lukas Wagner
  2025-07-30 11:21     ` Lukas Wagner
  2025-07-31  3:23     ` Thomas Lamprecht
  0 siblings, 2 replies; 50+ messages in thread
From: Lukas Wagner @ 2025-07-29  9:44 UTC (permalink / raw)
  To: Proxmox VE development discussion, Aaron Lauterer; +Cc: pve-devel

Hey Aaron,

some notes inline.

The memory leaks I mentioned should be fixed before this goes in (since
this is in the long-running pmxcfs process), everything else could also
happen as a followup. I'm not super familiar with this code, so there
might be things that I've missed.

On Sat Jul 26, 2025 at 3:05 AM CEST, Aaron Lauterer wrote:
> With PVE9 now we have additional fields in the metrics that are
> collected and distributed in the cluster. The new fields/columns are
> added at the end of the existing ones. This makes it possible for PVE8
> installations to still use them by cutting the new additional data.
>
> To make it more future proof, the format of the keys for each metrics
> are now changed:
>
> Old pre PVE9:  pve{version}-{type}/{id}
> Now with PVE9: pve-{type}-{version}/{id}
>
> This way we have an easier time to handle new versions in the future as
> we initially only need to check for `pve-{type}-`. If we know the
> version, we can handle it accordingly; e.g. pad if older format with
> missing data. If we don't know the version, it must be a newer one and
> we cut the data stream at the length we need for the current version.
>
> This means of course that to avoid a breaking change, we can only add
> new columns if needed, but not remove any! But waiting for a breaking
> change until the next major release is a worthy trade-off if it allows
> us to expand the format in between if needed.
>
> The 'rrd_skip_data' function got a new parameter defining the sepataring
> character. This then makes it possible to use it also to determine which
> part of the key string is the version/type and which one is the actual
> resource identifier.
>
> We add several new columns to nodes and VMs (guest) RRDs. See futher
> down for details. Additionally we change the RRA definitions on how we
> aggregate the data to match how we do it for the Proxmox Backup Server
> [0].
>
> The migration of an existing installation is handled by a dedicated
> tool. Only once that has happened, will we store data in the new
> format.
> This leaves us with a few cases to handle:
>
>   data recv →          old                                 new
>   ↓ rrd files
>  -------------|---------------------------|-------------------------------------
>   none        | check if directories exists:
>               |     neither old or new -> new
> 	      |     new                -> new
> 	      |     old only           -> old
> --------------|---------------------------|-------------------------------------
>   only old    | use old file as is        | cut new columns and use old file
> --------------|---------------------------|-------------------------------------
>   new present | pad data to match new fmt | use new file as is and pass data
>
> To handle the padding we use a buffer. Cutting can be handled as we
> already do it in the stable/bookworm (PVE8) branch by introducing a null
> terminator in the original string at the end of the expected columns.
>
> We add the following new columns:
>
> Nodes:
> * memfree
> * arcsize
> * pressures:
>   * cpu some
>   * io some
>   * io full
>   * mem some
>   * mem full
>
> VMs:
> * memhost (memory consumption of all processes in the guests cgroup, host view)
> * pressures:
>   * cpu some
>   * cpu full
>   * io some
>   * io full
>   * mem some
>   * mem full
>
> [0] https://git.proxmox.com/?p=proxmox-backup.git;a=blob;f=src/server/metric_collection/rrd.rs;h=ed39cc94ee056924b7adbc21b84c0209478bcf42;hb=dc324716a688a67d700fa133725740ac5d3795ce#l76
>
> Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
> ---
>  src/pmxcfs/status.c | 261 +++++++++++++++++++++++++++++++++++++++-----
>  1 file changed, 236 insertions(+), 25 deletions(-)
>
> diff --git a/src/pmxcfs/status.c b/src/pmxcfs/status.c
> index e6b578b..eaef12c 100644
> --- a/src/pmxcfs/status.c
> +++ b/src/pmxcfs/status.c
> @@ -1096,6 +1096,9 @@ kventry_hash_set(GHashTable *kvhash, const char *key, gconstpointer data, size_t
>      return TRUE;
>  }
>  
> +// We create the RRD files with a 60 second stepsize, therefore, RRA timesteps
> +// are alwys per 60 seconds. These 60 seconds are usually showing up in other
> +// code paths where we interact with RRD data!
>  static const char *rrd_def_node[] = {
>      "DS:loadavg:GAUGE:120:0:U",
>      "DS:maxcpu:GAUGE:120:0:U",
> @@ -1124,6 +1127,39 @@ static const char *rrd_def_node[] = {
>      NULL,
>  };
>  
> +static const char *rrd_def_node_pve9_0[] = {
> +    "DS:loadavg:GAUGE:120:0:U",
> +    "DS:maxcpu:GAUGE:120:0:U",
> +    "DS:cpu:GAUGE:120:0:U",
> +    "DS:iowait:GAUGE:120:0:U",
> +    "DS:memtotal:GAUGE:120:0:U",
> +    "DS:memused:GAUGE:120:0:U",
> +    "DS:swaptotal:GAUGE:120:0:U",
> +    "DS:swapused:GAUGE:120:0:U",
> +    "DS:roottotal:GAUGE:120:0:U",
> +    "DS:rootused:GAUGE:120:0:U",
> +    "DS:netin:DERIVE:120:0:U",
> +    "DS:netout:DERIVE:120:0:U",
> +    "DS:memfree:GAUGE:120:0:U",
> +    "DS:arcsize:GAUGE:120:0:U",
> +    "DS:pressurecpusome:GAUGE:120:0:U",
> +    "DS:pressureiosome:GAUGE:120:0:U",
> +    "DS:pressureiofull:GAUGE:120:0:U",
> +    "DS:pressurememorysome:GAUGE:120:0:U",
> +    "DS:pressurememoryfull:GAUGE:120:0:U",
> +
> +    "RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
> +    "RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
> +    "RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
> +    "RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
> +
> +    "RRA:MAX:0.5:1:1440",    // 1 min * 1440 => 1 day
> +    "RRA:MAX:0.5:30:1440",   // 30 min * 1440 => 30 day
> +    "RRA:MAX:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
> +    "RRA:MAX:0.5:10080:570", // 1 week * 570 => ~10 years
> +    NULL,
> +};
> +
>  static const char *rrd_def_vm[] = {
>      "DS:maxcpu:GAUGE:120:0:U",
>      "DS:cpu:GAUGE:120:0:U",
> @@ -1149,6 +1185,36 @@ static const char *rrd_def_vm[] = {
>      "RRA:MAX:0.5:10080:70", // 7 day max - ony year
>      NULL,
>  };
> +static const char *rrd_def_vm_pve9_0[] = {
> +    "DS:maxcpu:GAUGE:120:0:U",
> +    "DS:cpu:GAUGE:120:0:U",
> +    "DS:maxmem:GAUGE:120:0:U",
> +    "DS:mem:GAUGE:120:0:U",
> +    "DS:maxdisk:GAUGE:120:0:U",
> +    "DS:disk:GAUGE:120:0:U",
> +    "DS:netin:DERIVE:120:0:U",
> +    "DS:netout:DERIVE:120:0:U",
> +    "DS:diskread:DERIVE:120:0:U",
> +    "DS:diskwrite:DERIVE:120:0:U",
> +    "DS:memhost:GAUGE:120:0:U",
> +    "DS:pressurecpusome:GAUGE:120:0:U",
> +    "DS:pressurecpufull:GAUGE:120:0:U",
> +    "DS:pressureiosome:GAUGE:120:0:U",
> +    "DS:pressureiofull:GAUGE:120:0:U",
> +    "DS:pressurememorysome:GAUGE:120:0:U",
> +    "DS:pressurememoryfull:GAUGE:120:0:U",
> +
> +    "RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
> +    "RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
> +    "RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
> +    "RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
> +
> +    "RRA:MAX:0.5:1:1440",    // 1 min * 1440 => 1 day
> +    "RRA:MAX:0.5:30:1440",   // 30 min * 1440 => 30 day
> +    "RRA:MAX:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
> +    "RRA:MAX:0.5:10080:570", // 1 week * 570 => ~10 years
> +    NULL,
> +};
>  
>  static const char *rrd_def_storage[] = {
>      "DS:total:GAUGE:120:0:U",
> @@ -1168,8 +1234,30 @@ static const char *rrd_def_storage[] = {
>      NULL,
>  };
>  
> +static const char *rrd_def_storage_pve9_0[] = {
> +    "DS:total:GAUGE:120:0:U",
> +    "DS:used:GAUGE:120:0:U",
> +
> +    "RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
> +    "RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
> +    "RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
> +    "RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
> +
> +    "RRA:MAX:0.5:1:1440",    // 1 min * 1440 => 1 day
> +    "RRA:MAX:0.5:30:1440",   // 30 min * 1440 => 30 day
> +    "RRA:MAX:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
> +    "RRA:MAX:0.5:10080:570", // 1 week * 570 => ~10 years
> +    NULL,
> +};

Not important right now, but the migration tool and this
should probably use the same source of the schema definition (e.g. via
putting this into a header file which is then shared in some way
(submodule?))

> +
>  #define RRDDIR "/var/lib/rrdcached/db"
>  
> +// A 4k buffer should be plenty to temporarily store RRD data. 64 bit integers are 20 chars long,
> +// plus the separator char: (4096-1)/21~195 columns This buffer is only used in the
> +// `update_rrd_data` function. It is safe to use as the calling sites get the global mutex:
> +// rrd_update_data -> rrdentry_hash_set -> cfs_status_set / and cfs_kvstore_node_set
> +static char rrd_format_update_buffer[4096];

Maybe I'm missing something, but since this is only used in the
update_rrd_data function, is there any reason why this statically
allocated and not just malloc'd as needed?

> +
>  static void create_rrd_file(const char *filename, int argcount, const char *rrddef[]) {
>      /* start at day boundary */
>      time_t ctime;
> @@ -1229,6 +1317,8 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
>  
>      int skip = 0; // columns to skip at beginning. They contain non-archivable data, like uptime,
>                    // status, is guest a template and such.
> +    int padding = 0; // how many columns need to be added with "U" if we get an old format that is
> +                     // missing columns at the end.
>      int keep_columns = 0; // how many columns do we want to keep (after initial skip) in case we get
>                            // more columns than needed from a newer format
>  
> @@ -1243,20 +1333,60 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
>              goto keyerror;
>          }
>  
> -        skip = 2; // first two columns are live data that isn't archived
> +        filename = g_strdup_printf(RRDDIR "/pve-node-9.0/%s", node);
> +        char *filename_pve2 = g_strdup_printf(RRDDIR "/pve2-node/%s", node);
>  
> -        if (strncmp(key, "pve-node-", 9) == 0) {

Btw, you can safely use `strcmp` if the thing you are comparing to is a
string literal, since these are guaranteed to be NULL-terminated. I
would even argue its better that using `strncmp`, since there is no
chance of miscounting the length of the string literal (which is also
hard to spot in code review).

Applies to the other instances where you do the same as well.

Of course, if *both* arguments are a pointer with content that might not
be NULL-terminated, you are still required to use `strnlen` of avoid
buffer overruns.




> -            keep_columns = 12; // pve2-node format uses 12 columns
> +        int use_pve2_file = 0;
> +
> +        // check existing rrd files and directories
> +        if (g_file_test(filename, G_FILE_TEST_EXISTS)) {
> +            // pve-node-9.0 file exists, we use that
> +            // TODO: get conditions so, that we do not have this empty branch
> +        } else if (g_file_test(filename_pve2, G_FILE_TEST_EXISTS)) {
> +            // old file exists, use it
> +            use_pve2_file = 1;
> +            filename = g_strdup_printf("%s", filename_pve2);


You are leaking memory here, `filename` already has a pointer to
allocated memory that you are overwriting.

> +        } else {
> +            // neither file exists, check for directories to decide and create file
> +            char *dir_pve2 = g_strdup_printf(RRDDIR "/pve2-node");
> +            char *dir_pve90 = g_strdup_printf(RRDDIR "/pve-node-9.0");
> +
> +            if (g_file_test(dir_pve90, G_FILE_TEST_IS_DIR)) {
> +
> +                int argcount = sizeof(rrd_def_node_pve9_0) / sizeof(void *) - 1;
> +                create_rrd_file(filename, argcount, rrd_def_node_pve9_0);
> +            } else if (g_file_test(dir_pve2, G_FILE_TEST_IS_DIR)) {
> +                use_pve2_file = 1;
> +
> +                filename = g_strdup_printf("%s", filename_pve2);

Same here

> +
> +                int argcount = sizeof(rrd_def_node) / sizeof(void *) - 1;
> +                create_rrd_file(filename, argcount, rrd_def_node);
> +            } else {
> +                // no dir exists yet, use new pve-node-9.0
> +                mkdir(RRDDIR "/pve-node-9.0", 0755);

Pre-existing, but mkdir returns an error code that should be checked

> +
> +                int argcount = sizeof(rrd_def_node_pve9_0) / sizeof(void *) - 1;
> +                create_rrd_file(filename, argcount, rrd_def_node_pve9_0);
> +            }
> +            g_free(dir_pve2);
> +            g_free(dir_pve90);
>          }
>  
> -        filename = g_strdup_printf(RRDDIR "/pve2-node/%s", node);
> +        skip = 2; // first two columns are live data that isn't archived
>  
> -        if (!g_file_test(filename, G_FILE_TEST_EXISTS)) {
> -            mkdir(RRDDIR "/pve2-node", 0755);
> -            int argcount = sizeof(rrd_def_node) / sizeof(void *) - 1;
> -            create_rrd_file(filename, argcount, rrd_def_node);
> +        if (strncmp(key, "pve2-node/", 10) == 0 && !use_pve2_file) {
> +            padding = 7; // pve-node-9.0 has 7 more columns than pve2-node
> +        } else if (strncmp(key, "pve-node-", 9) == 0 && use_pve2_file) {
> +            keep_columns = 12; // pve2-node format uses 12 columns
> +        } else if (strncmp(key, "pve-node-9.0/", 13) != 0) {
> +            // we received an unknown format, expectation is it is newer and has more columns
> +            // than we can currently handle
> +            keep_columns = 19; // pve-node-9.0 format uses 19 columns

I think these magic numbers should be constants, e.g.
  #define PVE_NODE_9_0_COLUMN_COUNT (19)

best defined next to rrd_def_node_pve9_0, so that it is easy to
cross-check with the rrd schema definition.

>          }
>  
> +        g_free(filename_pve2);
> +
>      } else if (strncmp(key, "pve2.3-vm/", 10) == 0 || strncmp(key, "pve-vm-", 7) == 0) {
>  
>          const char *vmid = rrd_skip_data(key, 1, '/');
> @@ -1269,20 +1399,60 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
>              goto keyerror;
>          }
>  
> -        skip = 4; // first 4 columns are live data that isn't archived
> +        filename = g_strdup_printf(RRDDIR "/pve-vm-9.0/%s", vmid);
> +        char *filename_pve2 = g_strdup_printf(RRDDIR "/%s/%s", "pve2-vm", vmid);
> +
> +        int use_pve2_file = 0;
> +
> +        // check existing rrd files and directories
> +        if (g_file_test(filename, G_FILE_TEST_EXISTS)) {
> +            // pve-vm-9.0 file exists, we use that
> +            // TODO: get conditions so, that we do not have this empty branch
> +        } else if (g_file_test(filename_pve2, G_FILE_TEST_EXISTS)) {
> +            // old file exists, use it
> +            use_pve2_file = 1;
> +            filename = g_strdup_printf("%s", filename_pve2);

Same here

> +        } else {
> +            // neither file exists, check for directories to decide and create file
> +            char *dir_pve2 = g_strdup_printf(RRDDIR "/pve2-vm");
> +            char *dir_pve90 = g_strdup_printf(RRDDIR "/pve-vm-9.0");
> +
> +            if (g_file_test(dir_pve90, G_FILE_TEST_IS_DIR)) {
>  
> -        if (strncmp(key, "pve-vm-", 7) == 0) {
> -            keep_columns = 10; // pve2.3-vm format uses 10 data columns
> +                int argcount = sizeof(rrd_def_vm_pve9_0) / sizeof(void *) - 1;
> +                create_rrd_file(filename, argcount, rrd_def_vm_pve9_0);
> +            } else if (g_file_test(dir_pve2, G_FILE_TEST_IS_DIR)) {
> +                use_pve2_file = 1;
> +
> +                filename = g_strdup_printf("%s", filename_pve2);

Same here

> +
> +                int argcount = sizeof(rrd_def_vm) / sizeof(void *) - 1;
> +                create_rrd_file(filename, argcount, rrd_def_vm);
> +            } else {
> +                // no dir exists yet, use new pve-vm-9.0
> +                mkdir(RRDDIR "/pve-vm-9.0", 0755);
> +
> +                int argcount = sizeof(rrd_def_vm_pve9_0) / sizeof(void *) - 1;
> +                create_rrd_file(filename, argcount, rrd_def_vm_pve9_0);
> +            }
> +            g_free(dir_pve2);
> +            g_free(dir_pve90);
>          }
>  
> -        filename = g_strdup_printf(RRDDIR "/%s/%s", "pve2-vm", vmid);
> +        skip = 4; // first 4 columns are live data that isn't archived
>  
> -        if (!g_file_test(filename, G_FILE_TEST_EXISTS)) {
> -            mkdir(RRDDIR "/pve2-vm", 0755);
> -            int argcount = sizeof(rrd_def_vm) / sizeof(void *) - 1;
> -            create_rrd_file(filename, argcount, rrd_def_vm);
> +        if (strncmp(key, "pve2.3-vm/", 10) == 0 && !use_pve2_file) {
> +            padding = 7; // pve-vm-9.0 has 7 more columns than pve2.3-vm
> +        } else if (strncmp(key, "pve-vm-", 7) == 0 && use_pve2_file) {
> +            keep_columns = 10; // pve2.3-vm format uses 10 columns
> +        } else if (strncmp(key, "pve-vm-9.0/", 11) != 0) {
> +            // we received an unknown format, expectation is it is newer and has more columns
> +            // than we can currently handle
> +            keep_columns = 17; // pve-vm-9.0 format uses 19 columns
>          }
>  
> +        g_free(filename_pve2);
> +
>      } else if (strncmp(key, "pve2-storage/", 13) == 0 || strncmp(key, "pve-storage-", 12) == 0) {
>          const char *node = rrd_skip_data(key, 1, '/'); // will contain {node}/{storage}
>  
> @@ -1300,18 +1470,50 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
>              goto keyerror;
>          }
>  
> -        filename = g_strdup_printf(RRDDIR "/pve2-storage/%s", node);
> +        filename = g_strdup_printf(RRDDIR "/pve-storage-9.0/%s", node);
> +        char *filename_pve2 = g_strdup_printf(RRDDIR "/%s/%s", "pve2-storage", node);
> +
> +        // check existing rrd files and directories
> +        if (g_file_test(filename, G_FILE_TEST_EXISTS)) {
> +            // pve-storage-9.0 file exists, we use that
> +            // TODO: get conditions so, that we do not have this empty branch
> +        } else if (g_file_test(filename_pve2, G_FILE_TEST_EXISTS)) {
> +            // old file exists, use it
> +            filename = g_strdup_printf("%s", filename_pve2);

Same here

> +        } else {
> +            // neither file exists, check for directories to decide and create file
> +            char *dir_pve2 = g_strdup_printf(RRDDIR "/pve2-storage");
> +            char *dir_pve90 = g_strdup_printf(RRDDIR "/pve-storage-9.0");
> +
> +            if (g_file_test(dir_pve90, G_FILE_TEST_IS_DIR)) {
> +
> +                int argcount = sizeof(rrd_def_storage_pve9_0) / sizeof(void *) - 1;
> +                create_rrd_file(filename, argcount, rrd_def_storage_pve9_0);
> +            } else if (g_file_test(dir_pve2, G_FILE_TEST_IS_DIR)) {
> +                filename = g_strdup_printf("%s", filename_pve2);

Same here

>  
> -        if (!g_file_test(filename, G_FILE_TEST_EXISTS)) {
> -            mkdir(RRDDIR "/pve2-storage", 0755);
> -            char *dir = g_path_get_dirname(filename);
> -            mkdir(dir, 0755);
> -            g_free(dir);
> +                int argcount = sizeof(rrd_def_storage) / sizeof(void *) - 1;
> +                create_rrd_file(filename, argcount, rrd_def_storage);
> +            } else {
> +                // no dir exists yet, use new pve-storage-9.0
> +                mkdir(RRDDIR "/pve-storage-9.0", 0755);
>  
> -            int argcount = sizeof(rrd_def_storage) / sizeof(void *) - 1;
> -            create_rrd_file(filename, argcount, rrd_def_storage);
> +                int argcount = sizeof(rrd_def_storage_pve9_0) / sizeof(void *) - 1;
> +                create_rrd_file(filename, argcount, rrd_def_storage_pve9_0);
> +            }
> +            g_free(dir_pve2);
> +            g_free(dir_pve90);
>          }
>  
> +        // actual data columns didn't change between pve2-storage and pve-storage-9.0
> +        if (strncmp(key, "pve-storage-", 12) == 0 && strncmp(key, "pve-storage-9.0/", 16) != 0) {
> +            // we received an unknown format, expectation is it is newer and has more columns
> +            // than we can currently handle
> +            keep_columns = 2; // pve-storage-9.0 format uses 2 columns
> +        }
> +
> +        g_free(filename_pve2);
> +
>      } else {
>          goto keyerror;
>      }
> @@ -1325,7 +1527,16 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
>          *(cut - 1) = 0; // terminate string by replacing colon from field separator with zero.
>      }
>  
> -    const char *update_args[] = {dp, NULL};
> +    const char *update_args[] = {NULL, NULL};
> +    if (padding) {
> +        // add padding "U" columns to data string
> +        char *padsrc =
> +            ":U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U:U"; // can pad up to 25 columns
> +        g_snprintf(rrd_format_update_buffer, 1024 * 4, "%s%.*s", dp, padding * 2, padsrc);
> +        update_args[0] = rrd_format_update_buffer;
> +    } else {
> +        update_args[0] = dp;
> +    }
>  
>      if (use_daemon) {
>          int status;



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH proxmox-rrd-migration-tool v4 3/3] add debian packaging
  2025-07-29  9:29     ` Thomas Lamprecht
@ 2025-07-29  9:49       ` Lukas Wagner
  0 siblings, 0 replies; 50+ messages in thread
From: Lukas Wagner @ 2025-07-29  9:49 UTC (permalink / raw)
  To: Proxmox VE development discussion, Thomas Lamprecht; +Cc: pve-devel

On Tue Jul 29, 2025 at 11:29 AM CEST, Thomas Lamprecht wrote:
> Am 28.07.25 um 16:36 schrieb Lukas Wagner:
>>> diff --git a/debian/links b/debian/links
>>> new file mode 100644
>>> index 0000000..9e59b57
>>> --- /dev/null
>>> +++ b/debian/links
>>> @@ -0,0 +1 @@
>>> +usr/libexec/proxmox/proxmox-rrd-migration-tool usr/bin/proxmox-rrd-migration-tool
>> As far as I understand this is a tool that should be called directly by
>> a user, right? In that case I'd not add the symlink but alter the
>> Makefile so that it installs it to /usr/bin/ right away.
>
> It actually shouldn't, the migration should be handled automatically and
> the tool only called manually in the edge case, in which providing the full
> path (e.g., in the respective 8to9 check output) is enough.

Ah yeah, wrote this comment before actually looking into the pve-manager
patches, missed that it is supposed to be called by d/postinst.

>
> So while you're definitively right in that this way is not gaining us
> anything (good catch!), I'd rather just drop the symnlink in /usr/bin and
> keep it only in libexec.

Agreed, in this case I'd also just drop the symlink.

Thanks for the clarification!



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH manager v4 14/15] d/postinst: run promox-rrd-migration-tool
  2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 14/15] d/postinst: run promox-rrd-migration-tool Aaron Lauterer
@ 2025-07-29 12:09   ` Lukas Wagner
  0 siblings, 0 replies; 50+ messages in thread
From: Lukas Wagner @ 2025-07-29 12:09 UTC (permalink / raw)
  To: Proxmox VE development discussion, Aaron Lauterer; +Cc: pve-devel

On Sat Jul 26, 2025 at 3:06 AM CEST, Aaron Lauterer wrote:
> Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
> ---
>
> Notes:
>     currently it checks for lt 9.0.0~12. should it only be applied to a
>     later version, don't forget to adapt the version check!
>     
>     I tested it by bumping the version to 9.0.0~12
>     upgraded to it -> migration ran
>     reinstalled -> no migration happening
>     
>     when installing the bumped pve-manager package and the
>     proxmox-rrd-migration-tool package at the same time, dependencies are
>     resolved and the postinst script works.
>     
>     There is still one bug though that happens on my live system: While the
>     migration tool moves the processed files to FILE.old, new ones without
>     the .old are still present.
>     I did a quick try, disabling rrdached before we call the migration tool.
>     But that didn't help. Could be that pmxcfs is receiving new data and is
>     recreating them. Or maybe something else.
>     That would need to be debugged to figure out as apparently I did miss
>     something here regarding the behavior.
>
>  debian/postinst | 5 +++++
>  1 file changed, 5 insertions(+)
>
> diff --git a/debian/postinst b/debian/postinst
> index a0480b24..b15603ac 100755
> --- a/debian/postinst
> +++ b/debian/postinst
> @@ -227,6 +227,11 @@ case "$1" in
>              migrate_apt_auth_conf
>          fi
>      fi
> +
> +    if test -n "$2" && dpkg --compare-versions "$2" 'lt' '9.0.0~12'; then
> +        echo "migradting RRD to new PVE format version - this can take some time!"

small typo in 'migrating'

> +        proxmox-rrd-migration-tool --migrate || echo "migration failed, see output above for errors and try to migrate existing data manually by running 'proxmox-rrd-migration-tool --migrate'"
> +    fi
>      ;;
>  
>    abort-upgrade|abort-remove|abort-deconfigure)



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (31 preceding siblings ...)
  2025-07-28 14:42 ` [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Thomas Lamprecht
@ 2025-07-29 12:19 ` Lukas Wagner
  2025-07-31  4:12 ` [pve-devel] applied: " Thomas Lamprecht
  33 siblings, 0 replies; 50+ messages in thread
From: Lukas Wagner @ 2025-07-29 12:19 UTC (permalink / raw)
  To: Proxmox VE development discussion, Aaron Lauterer

On Sat Jul 26, 2025 at 3:05 AM CEST, Aaron Lauterer wrote:
> This patch series does a few things. It expands the RRD format for nodes and
>  VMs. For all types (nodes, VMs, storage) we adjust the aggregation to align
>  them with the way they are done on the Backup Server. Therefore, we have new
>  RRD defitions for all 3 types.
>
> New values are added for nodes and VMs. In particular:
>
> Nodes:
> * memfree
> * arcsize
> * pressures:
>   * cpu some
>   * io some
>   * io full
>   * mem some
>   * mem full
>
> VMs:
> * memhost (memory consumption of all processes in the guests cgroup, host view)
> * pressures:
>   * cpu some
>   * cpu full
>   * io some
>   * io full
>   * mem some
>   * mem full
>
> The change in RRD columns and aggregation means, that we need new RRD files. To
> not lose old RRD data, we need to migrate the old RRD files to the ones with
> the new schema. Some initial performance tests showed that migrating 10k VM
> RRD files took ~2m40s single threaded. This is way to long to do it within the
> pmxcfs itself. Therefore this will be a dedicated step:
> The new `proxmox-rrd-migration-tool` migrates the RRD files to the new location
> and aggregation schemas. It is run automatically by the postinst script of the
> pve-manager.
>
> This also means, that we need to handle the situation of new and old RRD
> files and formats. Therefore we introduce new keys by which the metrics
> are broadcast in a cluster. Up until now (pre PVE9), it is in the format of
> 'pve2-{type}/{resource id}'.
> Having the version number this early in the string makes it tough to match
> against newer ones, especially in the C code of the pmxcfs. To make it easier
> in the future, we change the key format to 'pve-{type}-{version}/{resource id}'.
> This way, we can fuzzy match against unknown 'pve-{type}-{version}' in the C
> code too and handle those situations better.
>
> The result is, that to avoid breaking changes, we are only allowed to add new
> columns, but not modify or remove existing columns!
>
>
> To avoid missing data and key errors in the journal, we already bumped 
> changes to PVE 8 so it can handle the new format sent out by pvestatd in the
> latest versions.
>
> On the GUI side, we switch memory graphs to stacked area graphs and for VMs
> we also have a dedicated line for the memory consumption as the host sees it.
> Because the current memory view of a VM will switch to the internal guest view,
> if we get detailed infos via the ballooning device.
> To make those slightly more complicated graphs possible, we need to adapt
> RRDChart.js in the widget-toolkit to allow for detailed overrides.
>
> While we are at it, we can also fix bug #6068 (Node Search tab incorrect Host
> memory usage %) by switching to memhost if available and one wrong if check.
>
>
> As a side note, now that we got pressure graphs, we could start thinking about
> dropping the server load and IO wait graphs. Those are not very specific and
> mash many different metrics into a single one.
>
>
> Release notes:
> We should probably mention in the release notes, that due to the changed
> aggregation settings, it is expected that the resulting RRD files might have
> some data points that the originals didn't have. We observed that in some
> situation we get could get a data point in one time step earlier than before.
> This is most likely due to how RRD recalculates the aggregated data with the
> different resolution.
>
> In the pve8to9 checks, we now have a check that makes sure we do have enough
> free space, as the new RRD files with the new columns and more detailed
> aggeration steps, are quite a bit larger. We also check after install, if any
> RRD files have not yet been migrated, which would warrant another manual run of
> the migration tool.
>
> Plans:
> * add doc patches for the summary pages that explain the different graphs and
> make the help button point to those sections
>
> KNOWN ISSUES:
> * on a live system, renaming the source RRD files to FILE.old doesn't seem to
> work as expected and besides the renamed ones, new ones without the .old prefix
> show up again. I suspect some interaction with rrdached and/or pmxcfs receiving
> new data.
>
> How to test:
> 1. have PVE8 nodes on the latest version (>= 8.4.4)
> 2. Upgrade the first node to PVE9/trixie and install all the other patches
>     to see the automatic upgrade, pve-manager might need to be temporarily
>     bumped to 9.0.0~12!
>     build all the other repositories, copy the .deb files over and then ideally
>     use something like the following to make shure that any dependency will be
>     used from the deb files, and not the apt repositories.
>     ```
>     apt install ./*.deb --reinstall --allow-downgrades -y
>     ```
> 3. you should see, if the pve-manager package calling the
> proxmox-rrd-migration-tool
>

Gave this series a test on a pre-existing 3-node dev cluster.
I first upgraded the cluster to PVE9 and then installed the packages
from this series on top. I could verify that the
proxmox-rrd-migration-tool is executed by pve-manager's d/postinst
script.

Now, unfortunately that was a cluster that I don't use that often, so
the weekly/monthly/yearly RRD data was pretty empty, but I can at least
verify that the hourly data was migrated successfully. Also I could see
the new pressure graphs being populated immediately after the upgrade.
Nice!

During testing I did not really encounter any issues.

Regarding the memleak in pmxcfs that I mentioned during my review of the
pve-cluster patches: I could definitely see RSS creep up quite slowly.
Not sure how much of that is due to the leak and how much is 'normal',
where the heap size slowly converges to some final maximum. I'll keep
the cluster running for a bit more time and see where this goes.

Consider this:

Tested-by: Lukas Wagner <l.wagner@proxmox.com>




_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH qemu-server v4 2/4] vmstatus: add memhost for host view of vm mem consumption
  2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 2/4] vmstatus: add memhost for host view of vm mem consumption Aaron Lauterer
@ 2025-07-29 12:49   ` Lukas Wagner
  2025-07-31  3:37     ` Thomas Lamprecht
  0 siblings, 1 reply; 50+ messages in thread
From: Lukas Wagner @ 2025-07-29 12:49 UTC (permalink / raw)
  To: Proxmox VE development discussion, Aaron Lauterer

On Sat Jul 26, 2025 at 3:06 AM CEST, Aaron Lauterer wrote:
> +        my $fh = IO::File->new("/sys/fs/cgroup/qemu.slice/${vmid}.scope/cgroup.procs", "r");
> +        if ($fh) {
> +            while (my $childPid = <$fh>) {
> +                chomp($childPid);

nit: should be snake_case

> +                open(my $SMAPS_FH, '<', "/proc/$childPid/smaps_rollup")
> +                    or die "failed to open PSS memory-stat from process - $!\n";
> +
> +                while (my $line = <$SMAPS_FH>) {
> +                    if ($line =~ m/^Pss:\s+([0-9]+) kB$/) {
> +                        $d->{memhost} = $d->{memhost} + int($1) * 1024;

Why do you sum up $d->{memhost} with the thing you just read from /proc?
As far as I can tell memhost should always be zero at this point....


> +                        last;

... and also you break here from the loop, so even if there were two Pss
lines, you wouldn't add them up.

Am I missing something? :)

> +                    }
> +                }
> +                close $SMAPS_FH;
> +            }
> +        }
> +        close($fh);
> +
>          my $pressures = PVE::ProcFSTools::read_cgroup_pressure("qemu.slice/${vmid}.scope");
>          $d->{pressurecpusome} = $pressures->{cpu}->{some}->{avg10} * 1;
>          $d->{pressurecpufull} = $pressures->{cpu}->{full}->{avg10} * 1;
> @@ -2707,7 +2732,6 @@ sub vmstatus {
>          } else {
>              $d->{cpu} = $old->{cpu};
>          }
> -
>      }
>  
>      return $res if !$full;



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH cluster v4 1/2] status: introduce new pve-{type}- rrd and metric format
  2025-07-29  9:44   ` Lukas Wagner
@ 2025-07-30 11:21     ` Lukas Wagner
  2025-07-31  3:23     ` Thomas Lamprecht
  1 sibling, 0 replies; 50+ messages in thread
From: Lukas Wagner @ 2025-07-30 11:21 UTC (permalink / raw)
  To: Proxmox VE development discussion, Lukas Wagner

On Tue Jul 29, 2025 at 11:44 AM CEST, Lukas Wagner wrote:
> Hey Aaron,
>
> some notes inline.
>
> The memory leaks I mentioned should be fixed before this goes in (since
> this is in the long-running pmxcfs process), everything else could also
> happen as a followup. I'm not super familiar with this code, so there
> might be things that I've missed.
>
> On Sat Jul 26, 2025 at 3:05 AM CEST, Aaron Lauterer wrote:
>> With PVE9 now we have additional fields in the metrics that are
>> collected and distributed in the cluster. The new fields/columns are
>> added at the end of the existing ones. This makes it possible for PVE8
>> installations to still use them by cutting the new additional data.
>>
>> To make it more future proof, the format of the keys for each metrics
>> are now changed:
>>
>> Old pre PVE9:  pve{version}-{type}/{id}
>> Now with PVE9: pve-{type}-{version}/{id}
>>
>> This way we have an easier time to handle new versions in the future as
>> we initially only need to check for `pve-{type}-`. If we know the
>> version, we can handle it accordingly; e.g. pad if older format with
>> missing data. If we don't know the version, it must be a newer one and
>> we cut the data stream at the length we need for the current version.
>>
>> This means of course that to avoid a breaking change, we can only add
>> new columns if needed, but not remove any! But waiting for a breaking
>> change until the next major release is a worthy trade-off if it allows
>> us to expand the format in between if needed.
>>
>> The 'rrd_skip_data' function got a new parameter defining the sepataring
>> character. This then makes it possible to use it also to determine which
>> part of the key string is the version/type and which one is the actual
>> resource identifier.
>>
>> We add several new columns to nodes and VMs (guest) RRDs. See futher
>> down for details. Additionally we change the RRA definitions on how we
>> aggregate the data to match how we do it for the Proxmox Backup Server
>> [0].
>>
>> The migration of an existing installation is handled by a dedicated
>> tool. Only once that has happened, will we store data in the new
>> format.
>> This leaves us with a few cases to handle:
>>
>>   data recv →          old                                 new
>>   ↓ rrd files
>>  -------------|---------------------------|-------------------------------------
>>   none        | check if directories exists:
>>               |     neither old or new -> new
>> 	      |     new                -> new
>> 	      |     old only           -> old
>> --------------|---------------------------|-------------------------------------
>>   only old    | use old file as is        | cut new columns and use old file
>> --------------|---------------------------|-------------------------------------
>>   new present | pad data to match new fmt | use new file as is and pass data
>>
>> To handle the padding we use a buffer. Cutting can be handled as we
>> already do it in the stable/bookworm (PVE8) branch by introducing a null
>> terminator in the original string at the end of the expected columns.
>>
>> We add the following new columns:
>>
>> Nodes:
>> * memfree
>> * arcsize
>> * pressures:
>>   * cpu some
>>   * io some
>>   * io full
>>   * mem some
>>   * mem full
>>
>> VMs:
>> * memhost (memory consumption of all processes in the guests cgroup, host view)
>> * pressures:
>>   * cpu some
>>   * cpu full
>>   * io some
>>   * io full
>>   * mem some
>>   * mem full
>>
>> [0] https://git.proxmox.com/?p=proxmox-backup.git;a=blob;f=src/server/metric_collection/rrd.rs;h=ed39cc94ee056924b7adbc21b84c0209478bcf42;hb=dc324716a688a67d700fa133725740ac5d3795ce#l76
>>
>> Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
>> ---
>>  src/pmxcfs/status.c | 261 +++++++++++++++++++++++++++++++++++++++-----
>>  1 file changed, 236 insertions(+), 25 deletions(-)
>>
>> diff --git a/src/pmxcfs/status.c b/src/pmxcfs/status.c
>> index e6b578b..eaef12c 100644
>> --- a/src/pmxcfs/status.c
>> +++ b/src/pmxcfs/status.c
>> @@ -1096,6 +1096,9 @@ kventry_hash_set(GHashTable *kvhash, const char *key, gconstpointer data, size_t
>>      return TRUE;
>>  }
>>  
>> +// We create the RRD files with a 60 second stepsize, therefore, RRA timesteps
>> +// are alwys per 60 seconds. These 60 seconds are usually showing up in other
>> +// code paths where we interact with RRD data!
>>  static const char *rrd_def_node[] = {
>>      "DS:loadavg:GAUGE:120:0:U",
>>      "DS:maxcpu:GAUGE:120:0:U",
>> @@ -1124,6 +1127,39 @@ static const char *rrd_def_node[] = {
>>      NULL,
>>  };
>>  
>> +static const char *rrd_def_node_pve9_0[] = {
>> +    "DS:loadavg:GAUGE:120:0:U",
>> +    "DS:maxcpu:GAUGE:120:0:U",
>> +    "DS:cpu:GAUGE:120:0:U",
>> +    "DS:iowait:GAUGE:120:0:U",
>> +    "DS:memtotal:GAUGE:120:0:U",
>> +    "DS:memused:GAUGE:120:0:U",
>> +    "DS:swaptotal:GAUGE:120:0:U",
>> +    "DS:swapused:GAUGE:120:0:U",
>> +    "DS:roottotal:GAUGE:120:0:U",
>> +    "DS:rootused:GAUGE:120:0:U",
>> +    "DS:netin:DERIVE:120:0:U",
>> +    "DS:netout:DERIVE:120:0:U",
>> +    "DS:memfree:GAUGE:120:0:U",
>> +    "DS:arcsize:GAUGE:120:0:U",
>> +    "DS:pressurecpusome:GAUGE:120:0:U",
>> +    "DS:pressureiosome:GAUGE:120:0:U",
>> +    "DS:pressureiofull:GAUGE:120:0:U",
>> +    "DS:pressurememorysome:GAUGE:120:0:U",
>> +    "DS:pressurememoryfull:GAUGE:120:0:U",
>> +
>> +    "RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
>> +    "RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
>> +    "RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
>> +    "RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
>> +
>> +    "RRA:MAX:0.5:1:1440",    // 1 min * 1440 => 1 day
>> +    "RRA:MAX:0.5:30:1440",   // 30 min * 1440 => 30 day
>> +    "RRA:MAX:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
>> +    "RRA:MAX:0.5:10080:570", // 1 week * 570 => ~10 years
>> +    NULL,
>> +};
>> +
>>  static const char *rrd_def_vm[] = {
>>      "DS:maxcpu:GAUGE:120:0:U",
>>      "DS:cpu:GAUGE:120:0:U",
>> @@ -1149,6 +1185,36 @@ static const char *rrd_def_vm[] = {
>>      "RRA:MAX:0.5:10080:70", // 7 day max - ony year
>>      NULL,
>>  };
>> +static const char *rrd_def_vm_pve9_0[] = {
>> +    "DS:maxcpu:GAUGE:120:0:U",
>> +    "DS:cpu:GAUGE:120:0:U",
>> +    "DS:maxmem:GAUGE:120:0:U",
>> +    "DS:mem:GAUGE:120:0:U",
>> +    "DS:maxdisk:GAUGE:120:0:U",
>> +    "DS:disk:GAUGE:120:0:U",
>> +    "DS:netin:DERIVE:120:0:U",
>> +    "DS:netout:DERIVE:120:0:U",
>> +    "DS:diskread:DERIVE:120:0:U",
>> +    "DS:diskwrite:DERIVE:120:0:U",
>> +    "DS:memhost:GAUGE:120:0:U",
>> +    "DS:pressurecpusome:GAUGE:120:0:U",
>> +    "DS:pressurecpufull:GAUGE:120:0:U",
>> +    "DS:pressureiosome:GAUGE:120:0:U",
>> +    "DS:pressureiofull:GAUGE:120:0:U",
>> +    "DS:pressurememorysome:GAUGE:120:0:U",
>> +    "DS:pressurememoryfull:GAUGE:120:0:U",
>> +
>> +    "RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
>> +    "RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
>> +    "RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
>> +    "RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
>> +
>> +    "RRA:MAX:0.5:1:1440",    // 1 min * 1440 => 1 day
>> +    "RRA:MAX:0.5:30:1440",   // 30 min * 1440 => 30 day
>> +    "RRA:MAX:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
>> +    "RRA:MAX:0.5:10080:570", // 1 week * 570 => ~10 years
>> +    NULL,
>> +};
>>  
>>  static const char *rrd_def_storage[] = {
>>      "DS:total:GAUGE:120:0:U",
>> @@ -1168,8 +1234,30 @@ static const char *rrd_def_storage[] = {
>>      NULL,
>>  };
>>  
>> +static const char *rrd_def_storage_pve9_0[] = {
>> +    "DS:total:GAUGE:120:0:U",
>> +    "DS:used:GAUGE:120:0:U",
>> +
>> +    "RRA:AVERAGE:0.5:1:1440",    // 1 min * 1440 => 1 day
>> +    "RRA:AVERAGE:0.5:30:1440",   // 30 min * 1440 => 30 day
>> +    "RRA:AVERAGE:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
>> +    "RRA:AVERAGE:0.5:10080:570", // 1 week * 570 => ~10 years
>> +
>> +    "RRA:MAX:0.5:1:1440",    // 1 min * 1440 => 1 day
>> +    "RRA:MAX:0.5:30:1440",   // 30 min * 1440 => 30 day
>> +    "RRA:MAX:0.5:360:1440",  // 6 hours * 1440 => 360 day ~1 year
>> +    "RRA:MAX:0.5:10080:570", // 1 week * 570 => ~10 years
>> +    NULL,
>> +};
>
> Not important right now, but the migration tool and this
> should probably use the same source of the schema definition (e.g. via
> putting this into a header file which is then shared in some way
> (submodule?))
>
>> +
>>  #define RRDDIR "/var/lib/rrdcached/db"
>>  
>> +// A 4k buffer should be plenty to temporarily store RRD data. 64 bit integers are 20 chars long,
>> +// plus the separator char: (4096-1)/21~195 columns This buffer is only used in the
>> +// `update_rrd_data` function. It is safe to use as the calling sites get the global mutex:
>> +// rrd_update_data -> rrdentry_hash_set -> cfs_status_set / and cfs_kvstore_node_set
>> +static char rrd_format_update_buffer[4096];
>
> Maybe I'm missing something, but since this is only used in the
> update_rrd_data function, is there any reason why this statically
> allocated and not just malloc'd as needed?
>
>> +
>>  static void create_rrd_file(const char *filename, int argcount, const char *rrddef[]) {
>>      /* start at day boundary */
>>      time_t ctime;
>> @@ -1229,6 +1317,8 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
>>  
>>      int skip = 0; // columns to skip at beginning. They contain non-archivable data, like uptime,
>>                    // status, is guest a template and such.
>> +    int padding = 0; // how many columns need to be added with "U" if we get an old format that is
>> +                     // missing columns at the end.
>>      int keep_columns = 0; // how many columns do we want to keep (after initial skip) in case we get
>>                            // more columns than needed from a newer format
>>  
>> @@ -1243,20 +1333,60 @@ static void update_rrd_data(const char *key, gconstpointer data, size_t len) {
>>              goto keyerror;
>>          }
>>  
>> -        skip = 2; // first two columns are live data that isn't archived
>> +        filename = g_strdup_printf(RRDDIR "/pve-node-9.0/%s", node);
>> +        char *filename_pve2 = g_strdup_printf(RRDDIR "/pve2-node/%s", node);
>>  
>> -        if (strncmp(key, "pve-node-", 9) == 0) {
>
> Btw, you can safely use `strcmp` if the thing you are comparing to is a
> string literal, since these are guaranteed to be NULL-terminated. I
> would even argue its better that using `strncmp`, since there is no
> chance of miscounting the length of the string literal (which is also
> hard to spot in code review).
>
> Applies to the other instances where you do the same as well.
>
> Of course, if *both* arguments are a pointer with content that might not
> be NULL-terminated, you are still required to use `strnlen` of avoid
> buffer overruns.
>

Please disregard this comment. While what I've said is true for checking
for a full string match, here you are actually checking for a prefix, so
strnlen is the correct choice here.

I guess we could use strlen to avoid the error potential of passing the
length explicitly. A modern compiler will optimize it anyway and compute
the length at compile time.

	strncmp(key, "pve-node-", strlen("pve-node-"))

To avoid the repetition this could then also be some constant.

But that's all not that important for now and can still be done later
on.


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] applied: [PATCH proxmox-rrd-migration-tool v4 3/3] add debian packaging
  2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 3/3] add debian packaging Aaron Lauterer
  2025-07-28 14:36   ` Lukas Wagner
@ 2025-07-30 17:57   ` Thomas Lamprecht
  1 sibling, 0 replies; 50+ messages in thread
From: Thomas Lamprecht @ 2025-07-30 17:57 UTC (permalink / raw)
  To: Proxmox VE development discussion, Aaron Lauterer

Am 26.07.25 um 03:07 schrieb Aaron Lauterer:
> based on the termproxy packaging. Nothing fancy so far.
> 
> Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
> ---
> 
> Notes:
>     I added the links to the repos even though they don't exist yet. So if
>     the package and repo name is to change. make sure to adapt those :)
> 
>  Cargo.toml           |  4 +-
>  Makefile             | 89 ++++++++++++++++++++++++++++++++++++++++++++
>  debian/changelog     |  5 +++
>  debian/control       | 27 ++++++++++++++
>  debian/copyright     | 19 ++++++++++
>  debian/docs          |  1 +
>  debian/links         |  1 +
>  debian/rules         | 30 +++++++++++++++
>  debian/source/format |  1 +
>  9 files changed, 175 insertions(+), 2 deletions(-)
>  create mode 100644 Makefile
>  create mode 100644 debian/changelog
>  create mode 100644 debian/control
>  create mode 100644 debian/copyright
>  create mode 100644 debian/docs
>  create mode 100644 debian/links
>  create mode 100755 debian/rules
>  create mode 100644 debian/source/format
> 
>

applied, with a few follow ups from Lukas (thanks!) and myself on top, thanks!


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH cluster v4 1/2] status: introduce new pve-{type}- rrd and metric format
  2025-07-29  9:44   ` Lukas Wagner
  2025-07-30 11:21     ` Lukas Wagner
@ 2025-07-31  3:23     ` Thomas Lamprecht
  1 sibling, 0 replies; 50+ messages in thread
From: Thomas Lamprecht @ 2025-07-31  3:23 UTC (permalink / raw)
  To: Proxmox VE development discussion, Lukas Wagner, Aaron Lauterer

Am 29.07.25 um 11:44 schrieb Lukas Wagner:
>> +// A 4k buffer should be plenty to temporarily store RRD data. 64 bit integers are 20 chars long,
>> +// plus the separator char: (4096-1)/21~195 columns This buffer is only used in the
>> +// `update_rrd_data` function. It is safe to use as the calling sites get the global mutex:
>> +// rrd_update_data -> rrdentry_hash_set -> cfs_status_set / and cfs_kvstore_node_set
>> +static char rrd_format_update_buffer[4096];
> Maybe I'm missing something, but since this is only used in the
> update_rrd_data function, is there any reason why this statically
> allocated and not just malloc'd as needed?

For the record: I nudged Aaron in that direction in a previous review, IMO a
small static buffer can sometimes be easier to handle compared to malloc, and
definitively could be more performant that frequent malloc+free sequences, but
performance probably is not really impacted here in any case.


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH qemu-server v4 2/4] vmstatus: add memhost for host view of vm mem consumption
  2025-07-29 12:49   ` Lukas Wagner
@ 2025-07-31  3:37     ` Thomas Lamprecht
  2025-07-31  6:51       ` Lukas Wagner
  0 siblings, 1 reply; 50+ messages in thread
From: Thomas Lamprecht @ 2025-07-31  3:37 UTC (permalink / raw)
  To: Proxmox VE development discussion, Lukas Wagner, Aaron Lauterer

Am 29.07.25 um 14:50 schrieb Lukas Wagner:
> On Sat Jul 26, 2025 at 3:06 AM CEST, Aaron Lauterer wrote:
>> +        my $fh = IO::File->new("/sys/fs/cgroup/qemu.slice/${vmid}.scope/cgroup.procs", "r");
>> +        if ($fh) {
>> +            while (my $childPid = <$fh>) {
>> +                chomp($childPid);
> 
> nit: should be snake_case

+1 and go into it's own method, I moved it in a follow-up

> 
>> +                open(my $SMAPS_FH, '<', "/proc/$childPid/smaps_rollup")
>> +                    or die "failed to open PSS memory-stat from process - $!\n";
>> +
>> +                while (my $line = <$SMAPS_FH>) {
>> +                    if ($line =~ m/^Pss:\s+([0-9]+) kB$/) {
>> +                        $d->{memhost} = $d->{memhost} + int($1) * 1024;
> 
> Why do you sum up $d->{memhost} with the thing you just read from /proc?
> As far as I can tell memhost should always be zero at this point....

it's a += and there are two loops, basically it sums up all Pss stats for
every process in a cgroup to get a somewhat correct total memory consumption
of that cgroup.

> 
> 
>> +                        last;
> 
> ... and also you break here from the loop, so even if there were two Pss
> lines, you wouldn't add them up.
> 
> Am I missing something? :)

This is smaps_rollup, not smaps, i.e. the former is a a summed up variant
of all separate mappings from the latter, so there is always only one Pss
entry in there.

See (need to search twice for "smaps_rollup", there is no better link anchor
in the vicinity):

https://docs.kernel.org/filesystems/proc.html#process-specific-subdirectories

> 
>> +                    }
>> +                }
>> +                close $SMAPS_FH;
>> +            }
>> +        }
>> +        close($fh);
>> +
>>          my $pressures = PVE::ProcFSTools::read_cgroup_pressure("qemu.slice/${vmid}.scope");
>>          $d->{pressurecpusome} = $pressures->{cpu}->{some}->{avg10} * 1;
>>          $d->{pressurecpufull} = $pressures->{cpu}->{full}->{avg10} * 1;
>> @@ -2707,7 +2732,6 @@ sub vmstatus {
>>          } else {
>>              $d->{cpu} = $old->{cpu};
>>          }
>> -
>>      }
>>  
>>      return $res if !$full;
> 
> 
> 
> _______________________________________________
> pve-devel mailing list
> pve-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
> 
> 



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* [pve-devel] applied: [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs
  2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
                   ` (32 preceding siblings ...)
  2025-07-29 12:19 ` Lukas Wagner
@ 2025-07-31  4:12 ` Thomas Lamprecht
  33 siblings, 0 replies; 50+ messages in thread
From: Thomas Lamprecht @ 2025-07-31  4:12 UTC (permalink / raw)
  To: Proxmox VE development discussion, Aaron Lauterer

Am 26.07.25 um 03:06 schrieb Aaron Lauterer:
> This patch series does a few things. It expands the RRD format for nodes and
>  VMs. For all types (nodes, VMs, storage) we adjust the aggregation to align
>  them with the way they are done on the Backup Server. Therefore, we have new
>  RRD defitions for all 3 types.


applied series with a bunch of follow-ups from Lukas (thanks!) and myself, thanks!

> KNOWN ISSUES:
> * on a live system, renaming the source RRD files to FILE.old doesn't seem to
> work as expected and besides the renamed ones, new ones without the .old prefix
> show up again. I suspect some interaction with rrdached and/or pmxcfs receiving
> new data.

FWIW, while I prepared a service to handle this on-boot, delaying that by more than
a one or two minutes is not ideal either, and while we could differentiate between
smaller and bigger setups (thus less or more time required for the migration), it
is probably simpler just doing it on the upgrade itself, albeit getting this
synchronized would be good too, maybe playing around with stopping rrdcached before
the upgrade might help, or make pmxcfs not commit anything if it sees a flag file
or the like, but it's only a short period of time of RRD metrics, nothing critical,
so I did not want to block this on that.

btw. It would be still very important to add a short section for this to the
upgrade guide, mentioning how one can continue the upgrade again later, not
all will see the message in the d/postinst output.


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

* Re: [pve-devel] [PATCH qemu-server v4 2/4] vmstatus: add memhost for host view of vm mem consumption
  2025-07-31  3:37     ` Thomas Lamprecht
@ 2025-07-31  6:51       ` Lukas Wagner
  0 siblings, 0 replies; 50+ messages in thread
From: Lukas Wagner @ 2025-07-31  6:51 UTC (permalink / raw)
  To: Proxmox VE development discussion, Thomas Lamprecht

On Thu Jul 31, 2025 at 5:37 AM CEST, Thomas Lamprecht wrote:
> Am 29.07.25 um 14:50 schrieb Lukas Wagner:
>> On Sat Jul 26, 2025 at 3:06 AM CEST, Aaron Lauterer wrote:
>>> +        my $fh = IO::File->new("/sys/fs/cgroup/qemu.slice/${vmid}.scope/cgroup.procs", "r");
>>> +        if ($fh) {
>>> +            while (my $childPid = <$fh>) {
>>> +                chomp($childPid);
>> 
>> nit: should be snake_case
>
> +1 and go into it's own method, I moved it in a follow-up
>
>> 
>>> +                open(my $SMAPS_FH, '<', "/proc/$childPid/smaps_rollup")
>>> +                    or die "failed to open PSS memory-stat from process - $!\n";
>>> +
>>> +                while (my $line = <$SMAPS_FH>) {
>>> +                    if ($line =~ m/^Pss:\s+([0-9]+) kB$/) {
>>> +                        $d->{memhost} = $d->{memhost} + int($1) * 1024;
>> 
>> Why do you sum up $d->{memhost} with the thing you just read from /proc?
>> As far as I can tell memhost should always be zero at this point....
>
> it's a += and there are two loops, basically it sums up all Pss stats for
> every process in a cgroup to get a somewhat correct total memory consumption
> of that cgroup.

I guess I must have missed that second loop, my bad. Thanks for the
clarification!
>
>> 
>> 
>>> +                        last;
>> 
>> ... and also you break here from the loop, so even if there were two Pss
>> lines, you wouldn't add them up.
>> 
>> Am I missing something? :)
>
> This is smaps_rollup, not smaps, i.e. the former is a a summed up variant
> of all separate mappings from the latter, so there is always only one Pss
> entry in there.
>
> See (need to search twice for "smaps_rollup", there is no better link anchor
> in the vicinity):
>
> https://docs.kernel.org/filesystems/proc.html#process-specific-subdirectories
>

Thanks, I'll have a look!



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 50+ messages in thread

end of thread, other threads:[~2025-07-31  6:49 UTC | newest]

Thread overview: 50+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-07-26  1:05 [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Aaron Lauterer
2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 1/3] create proxmox-rrd-migration-tool Aaron Lauterer
2025-07-28 14:25   ` Lukas Wagner
2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 2/3] add first tests Aaron Lauterer
2025-07-28 14:52   ` Lukas Wagner
2025-07-26  1:05 ` [pve-devel] [PATCH proxmox-rrd-migration-tool v4 3/3] add debian packaging Aaron Lauterer
2025-07-28 14:36   ` Lukas Wagner
2025-07-29  9:29     ` Thomas Lamprecht
2025-07-29  9:49       ` Lukas Wagner
2025-07-30 17:57   ` [pve-devel] applied: " Thomas Lamprecht
2025-07-26  1:05 ` [pve-devel] [PATCH cluster v4 1/2] status: introduce new pve-{type}- rrd and metric format Aaron Lauterer
2025-07-29  9:44   ` Lukas Wagner
2025-07-30 11:21     ` Lukas Wagner
2025-07-31  3:23     ` Thomas Lamprecht
2025-07-26  1:06 ` [pve-devel] [PATCH cluster v4 2/2] rrd: adapt to new RRD format with different aggregation windows Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 1/4] rrdchart: allow to override the series object Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 2/4] rrdchart: use reference for undo button Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 3/4] rrdchard: set cursor pointer for legend Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH widget-toolkit v4 4/4] rrdchart: add dummy listener for legend clicks Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 01/15] pvestatd: collect and distribute new pve-{type}-9.0 metrics Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 02/15] api: nodes: rrd and rrddata add decade option and use new pve-node-9.0 rrd files Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 03/15] api2tools: extract_vm_status add new vm memhost column Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 04/15] ui: rrdmodels: add new columns and update existing Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 05/15] ui: node summary: use stacked memory graph with zfs arc Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 06/15] ui: add pressure graphs to node and guest summary Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 07/15] ui: GuestStatusView: add memhost for VM guests Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 08/15] ui: GuestSummary: memory switch to stacked and add hostmem Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 09/15] ui: GuestSummary: remember visibility of host memory view Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 10/15] ui: nodesummary: guestsummary: add tooltip info buttons Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 11/15] ui: summaries: use titles for disk and network series Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 12/15] fix #6068: ui: utils: calculate and render host memory usage correctly Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 13/15] d/control: require proxmox-rrd-migration-tool >= 1.0.0 Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH manager v4 14/15] d/postinst: run promox-rrd-migration-tool Aaron Lauterer
2025-07-29 12:09   ` Lukas Wagner
2025-07-26  1:06 ` [pve-devel] [PATCH manager stabe-8+master v4 15/15] pve8to9: add checkfs for RRD migration Aaron Lauterer
2025-07-29  8:15   ` Lukas Wagner
2025-07-29  9:16     ` Thomas Lamprecht
2025-07-26  1:06 ` [pve-devel] [PATCH storage v4 1/1] status: rrddata: use new pve-storage-9.0 rrd location if file is present Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 1/4] metrics: add pressure to metrics Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 2/4] vmstatus: add memhost for host view of vm mem consumption Aaron Lauterer
2025-07-29 12:49   ` Lukas Wagner
2025-07-31  3:37     ` Thomas Lamprecht
2025-07-31  6:51       ` Lukas Wagner
2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 3/4] vmstatus: switch mem stat to PSS of VM cgroup Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH qemu-server v4 4/4] rrddata: use new pve-vm-9.0 rrd location if file is present Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH container v4 1/2] metrics: add pressures to metrics Aaron Lauterer
2025-07-26  1:06 ` [pve-devel] [PATCH container v4 2/2] rrddata: use new pve-vm-9.0 rrd location if file is present Aaron Lauterer
2025-07-28 14:42 ` [pve-devel] [PATCH many v4 00/31] Expand and migrate RRD data and add/change summary graphs Thomas Lamprecht
2025-07-29 12:19 ` Lukas Wagner
2025-07-31  4:12 ` [pve-devel] applied: " Thomas Lamprecht

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