[PATCH ha-manager v3 36/40] test: add resource bundle generation test cases

Daniel Kral
2026-03-30T14:45:18Z

These test cases document which resource bundles count as active and
stationary and ensure that get_active_stationary_resource_bundles(...)
does produce the correct active, stationary resource bundles.

This is especially important, because these resource bundles are used
for the load balancing candidate generation, which is passed to
score_best_balancing_migration_candidates($candidates, ...). The
PVE::HA::Usage::{Static,Dynamic} implementation validates these
candidates and fails with an user-visible error message.

Signed-off-by: Daniel Kral <d.kral@proxmox.com>
---
changes v2 -> v3:
- none

src/test/Makefile | 1 +
src/test/test_resource_bundles.pl | 234 ++++++++++++++++++++++++++++++
2 files changed, 235 insertions(+)
create mode 100755 src/test/test_resource_bundles.pl

diff --git a/src/test/Makefile b/src/test/Makefile
index 6da9e100..f72b755b 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -6,6 +6,7 @@ test:
@echo "-- start regression tests --"
./test_failover1.pl
./test_rules_config.pl
+ ./test_resource_bundles.pl
./ha-tester.pl
./test_fence_config.pl
@echo "-- end regression tests (success) --"
diff --git a/src/test/test_resource_bundles.pl b/src/test/test_resource_bundles.pl
new file mode 100755
index 00000000..d38dc516
--- /dev/null
+++ b/src/test/test_resource_bundles.pl
@@ -0,0 +1,234 @@
+#!/usr/bin/perl
+
+use v5.36;
+
+use lib qw(..);
+
+use Test::More;
+
+use PVE::HA::Manager;
+
+my $get_active_stationary_resource_bundle_tests = [
+ {
+ description => "trivial resource bundles",
+ services => {
+ 'vm:101' => {
+ state => 'started',
+ node => 'node1',
+ },
+ 'vm:102' => {
+ state => 'started',
+ node => 'node1',
+ },
+ },
+ resource_affinity => {
+ positive => {},
+ negative => {},
+ },
+ resource_bundles => {
+ 'vm:101' => [
+ 'vm:101',
+ ],
+ 'vm:102' => [
+ 'vm:102',
+ ],
+ },
+ },
+ {
+ description => "simple resource bundle",
+ services => {
+ 'vm:101' => {
+ state => 'started',
+ node => 'node1',
+ },
+ 'vm:102' => {
+ state => 'started',
+ node => 'node1',
+ },
+ },
+ resource_affinity => {
+ positive => {
+ 'vm:101' => {
+ 'vm:102' => 1,
+ },
+ 'vm:102' => {
+ 'vm:101' => 1,
+ },
+ },
+ negative => {},
+ },
+ resource_bundles => {
+ 'vm:101' => [
+ 'vm:101', 'vm:102',
+ ],
+ },
+ },
+ {
+ description => "resource bundle with first resource stopped",
+ services => {
+ 'vm:101' => {
+ state => 'stopped',
+ node => 'node1',
+ },
+ 'vm:102' => {
+ state => 'started',
+ node => 'node1',
+ },
+ 'vm:103' => {
+ state => 'started',
+ node => 'node1',
+ },
+ },
+ resource_affinity => {
+ positive => {
+ 'vm:101' => {
+ 'vm:102' => 1,
+ 'vm:103' => 1,
+ },
+ 'vm:102' => {
+ 'vm:101' => 1,
+ 'vm:103' => 1,
+ },
+ 'vm:103' => {
+ 'vm:101' => 1,
+ 'vm:102' => 1,
+ },
+ },
+ negative => {},
+ },
+ resource_bundles => {
+ 'vm:102' => [
+ 'vm:102', 'vm:103',
+ ],
+ },
+ },
+ {
+ description => "resource bundle with some stopped resources",
+ services => {
+ 'vm:101' => {
+ state => 'started',
+ node => 'node1',
+ },
+ 'vm:102' => {
+ state => 'stopped',
+ node => 'node1',
+ },
+ 'vm:103' => {
+ state => 'started',
+ node => 'node1',
+ },
+ },
+ resource_affinity => {
+ positive => {
+ 'vm:101' => {
+ 'vm:102' => 1,
+ 'vm:103' => 1,
+ },
+ 'vm:102' => {
+ 'vm:101' => 1,
+ 'vm:103' => 1,
+ },
+ 'vm:103' => {
+ 'vm:101' => 1,
+ 'vm:102' => 1,
+ },
+ },
+ negative => {},
+ },
+ resource_bundles => {
+ 'vm:101' => [
+ 'vm:101', 'vm:103',
+ ],
+ },
+ },
+ {
+ description => "resource bundle with moving resources",
+ services => {
+ 'vm:101' => {
+ state => 'started',
+ node => 'node1',
+ },
+ 'vm:102' => {
+ state => 'migrate',
+ node => 'node2',
+ target => 'node1',
+ },
+ 'vm:103' => {
+ state => 'relocate',
+ node => 'node3',
+ target => 'node1',
+ },
+ },
+ resource_affinity => {
+ positive => {
+ 'vm:101' => {
+ 'vm:102' => 1,
+ 'vm:103' => 1,
+ },
+ 'vm:102' => {
+ 'vm:101' => 1,
+ 'vm:103' => 1,
+ },
+ 'vm:103' => {
+ 'vm:101' => 1,
+ 'vm:102' => 1,
+ },
+ },
+ negative => {},
+ },
+ resource_bundles => {},
+ },
+ # might happen if the resource bundle is generated even before the HA Manager
+ # puts the HA resources in migrate/relocate to make them adhere to the HA rules
+ {
+ description => "resource bundle with resources on different nodes",
+ services => {
+ 'vm:101' => {
+ state => 'started',
+ node => 'node1',
+ },
+ 'vm:102' => {
+ state => 'started',
+ node => 'node2',
+ },
+ 'vm:103' => {
+ state => 'started',
+ node => 'node3',
+ },
+ },
+ resource_affinity => {
+ positive => {
+ 'vm:101' => {
+ 'vm:102' => 1,
+ 'vm:103' => 1,
+ },
+ 'vm:102' => {
+ 'vm:101' => 1,
+ 'vm:103' => 1,
+ },
+ 'vm:103' => {
+ 'vm:101' => 1,
+ 'vm:102' => 1,
+ },
+ },
+ negative => {},
+ },
+ resource_bundles => {},
+ },
+];
+
+my $tests = [
+ @$get_active_stationary_resource_bundle_tests,
+];
+
+plan(tests => scalar($tests->@*));
+
+for my $case ($get_active_stationary_resource_bundle_tests->@*) {
+ my ($ss, $resource_affinity) = $case->@{qw(services resource_affinity)};
+
+ my $result = PVE::HA::Manager::get_active_stationary_resource_bundles($ss, $resource_affinity);
+
+ is_deeply($result, $case->{resource_bundles}, $case->{description});
+}
+
+done_testing();
--
2.47.3