[pve-devel] [PATCH ha-manager v2 10/12] rules: restrict inter-plugin resource references to simple cases

Daniel Kral d.kral at proxmox.com
Fri Aug 1 18:22:25 CEST 2025


Add inter-plugin checks and helpers, which allow resources to be used in
node affinity rules and resource affinity rules at the same time, if the
following conditions are met:

- the resources of a resource affinity rule are not part of any node
  affinity rule, which has multiple priority groups. This is because of
  the dynamic nature of priority groups.

- the resources of a positive resource affinity rule are part of at most
  one node affinity rule, but no more. Otherwise, it is not easily
  decidable (yet) what the common node restrictions are.

- the positive resource affinity rules, which have at least one resource
  which is part of one node affinity rule, make all the resources part
  of the node affinity rule.

- the resources of a negative resource affinity rule are not restricted
  by their node affinity rules in such a way that these do not have
  enough nodes to be separated on.

Additionally, resources of a positive resource affinity rule, which are
also part of at most a single node affinity rule, are also added to the
node affinity rule.

Signed-off-by: Daniel Kral <d.kral at proxmox.com>
---
 src/PVE/HA/Rules.pm | 281 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 281 insertions(+)

diff --git a/src/PVE/HA/Rules.pm b/src/PVE/HA/Rules.pm
index 64dae1e4..323ad038 100644
--- a/src/PVE/HA/Rules.pm
+++ b/src/PVE/HA/Rules.pm
@@ -410,6 +410,8 @@ sub canonicalize : prototype($$$) {
         next if $@; # plugin doesn't implement plugin_canonicalize(...)
     }
 
+    $class->global_canonicalize($rules);
+
     return $messages;
 }
 
@@ -475,4 +477,283 @@ sub get_next_ordinal : prototype($) {
     return $current_order + 1;
 }
 
+=head1 INTER-PLUGIN RULE CHECKERS
+
+=cut
+
+my $has_multiple_priorities = sub {
+    my ($node_affinity_rule) = @_;
+
+    my $priority;
+    for my $node (values $node_affinity_rule->{nodes}->%*) {
+        $priority = $node->{priority} if !defined($priority);
+
+        return 1 if $priority != $node->{priority};
+    }
+};
+
+=head3 check_single_priority_node_affinity_in_resource_affinity_rules(...)
+
+Returns all rules in C<$resource_affinity_rules> and C<$node_affinity_rules> as
+a list of lists, each consisting of the rule type and resource id, where at
+least one resource in a resource affinity rule are in node affinity rules,
+which have multiple priority groups defined.
+
+That is, the resource affinity rule cannot be statically checked to be feasible
+as the selection of the priority group is dependent on the currently online
+nodes.
+
+If there are none, the returned list is empty.
+
+=cut
+
+sub check_single_priority_node_affinity_in_resource_affinity_rules {
+    my ($resource_affinity_rules, $node_affinity_rules) = @_;
+
+    my @conflicts = ();
+
+    while (my ($resource_affinity_id, $resource_affinity_rule) = each %$resource_affinity_rules) {
+        my $has_conflicts;
+        my $resources = $resource_affinity_rule->{resources};
+        my @paired_node_affinity_rules = ();
+
+        for my $node_affinity_id (keys %$node_affinity_rules) {
+            my $node_affinity_rule = $node_affinity_rules->{$node_affinity_id};
+
+            next if sets_are_disjoint($resources, $node_affinity_rule->{resources});
+
+            $has_conflicts = $has_multiple_priorities->($node_affinity_rule)
+                if !$has_conflicts;
+
+            push @paired_node_affinity_rules, $node_affinity_id;
+        }
+        if ($has_conflicts) {
+            push @conflicts, ['resource-affinity', $resource_affinity_id];
+            push @conflicts, ['node-affinity', $_] for @paired_node_affinity_rules;
+        }
+    }
+
+    @conflicts = sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] } @conflicts;
+    return \@conflicts;
+}
+
+__PACKAGE__->register_check(
+    sub {
+        my ($args) = @_;
+
+        return check_single_priority_node_affinity_in_resource_affinity_rules(
+            $args->{resource_affinity_rules},
+            $args->{node_affinity_rules},
+        );
+    },
+    sub {
+        my ($conflicts, $errors) = @_;
+
+        for my $conflict (@$conflicts) {
+            my ($type, $ruleid) = @$conflict;
+
+            if ($type eq 'node-affinity') {
+                push $errors->{$ruleid}->{resources}->@*,
+                    "resources are in a resource affinity rule and cannot be in"
+                    . " a node affinity rule with multiple priorities";
+            } elsif ($type eq 'resource-affinity') {
+                push $errors->{$ruleid}->{resources}->@*,
+                    "resources are in node affinity rules with multiple priorities";
+            }
+        }
+    },
+);
+
+=head3 check_single_node_affinity_per_positive_resource_affinity_rule(...)
+
+Returns all rules in C<$positive_rules> and C<$node_affinity_rules> as a list of
+lists, each consisting of the rule type and resource id, where one of the
+resources is used in a positive resource affinity rule and more than one node
+affinity rule.
+
+If there are none, the returned list is empty.
+
+=cut
+
+sub check_single_node_affinity_per_positive_resource_affinity_rule {
+    my ($positive_rules, $node_affinity_rules) = @_;
+
+    my @conflicts = ();
+
+    while (my ($positiveid, $positive_rule) = each %$positive_rules) {
+        my $positive_resources = $positive_rule->{resources};
+        my @paired_node_affinity_rules = ();
+
+        while (my ($node_affinity_id, $node_affinity_rule) = each %$node_affinity_rules) {
+            next if sets_are_disjoint($positive_resources, $node_affinity_rule->{resources});
+
+            push @paired_node_affinity_rules, $node_affinity_id;
+        }
+        if (@paired_node_affinity_rules > 1) {
+            push @conflicts, ['positive', $positiveid];
+            push @conflicts, ['node-affinity', $_] for @paired_node_affinity_rules;
+        }
+    }
+
+    @conflicts = sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] } @conflicts;
+    return \@conflicts;
+}
+
+__PACKAGE__->register_check(
+    sub {
+        my ($args) = @_;
+
+        return check_single_node_affinity_per_positive_resource_affinity_rule(
+            $args->{positive_rules},
+            $args->{node_affinity_rules},
+        );
+    },
+    sub {
+        my ($conflicts, $errors) = @_;
+
+        for my $conflict (@$conflicts) {
+            my ($type, $ruleid) = @$conflict;
+
+            if ($type eq 'positive') {
+                push $errors->{$ruleid}->{resources}->@*,
+                    "resources are in multiple node affinity rules";
+            } elsif ($type eq 'node-affinity') {
+                push $errors->{$ruleid}->{resources}->@*,
+                    "at least one resource is in a positive resource affinity"
+                    . " rule and there are other resources in at least one"
+                    . " other node affinity rule already";
+            }
+        }
+    },
+);
+
+=head3 check_negative_resource_affinity_node_affinity_consistency(...)
+
+Returns all rules in C<$negative_rules> and C<$node_affinity_rules> as a list
+of lists, each consisting of the rule type and resource id, where the resources
+in the negative resource affinity rule are restricted to less nodes than needed
+to keep them separate by their node affinity rules.
+
+That is, the negative resource affinity rule cannot be fullfilled as there are
+not enough nodes to spread the resources on.
+
+If there are none, the returned list is empty.
+
+=cut
+
+sub check_negative_resource_affinity_node_affinity_consistency {
+    my ($negative_rules, $node_affinity_rules) = @_;
+
+    my @conflicts = ();
+
+    while (my ($negativeid, $negative_rule) = each %$negative_rules) {
+        my $allowed_nodes = {};
+        my $located_resources;
+        my $resources = $negative_rule->{resources};
+        my @paired_node_affinity_rules = ();
+
+        for my $node_affinity_id (keys %$node_affinity_rules) {
+            my ($node_affinity_resources, $node_affinity_nodes) =
+                $node_affinity_rules->{$node_affinity_id}->@{qw(resources nodes)};
+            my $common_resources = set_intersect($resources, $node_affinity_resources);
+
+            next if keys %$common_resources < 1;
+
+            $located_resources = set_union($located_resources, $common_resources);
+            $allowed_nodes = set_union($allowed_nodes, $node_affinity_nodes);
+
+            push @paired_node_affinity_rules, $node_affinity_id;
+        }
+        if (keys %$allowed_nodes < keys %$located_resources) {
+            push @conflicts, ['negative', $negativeid];
+            push @conflicts, ['node-affinity', $_] for @paired_node_affinity_rules;
+        }
+    }
+
+    @conflicts = sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] } @conflicts;
+    return \@conflicts;
+}
+
+__PACKAGE__->register_check(
+    sub {
+        my ($args) = @_;
+
+        return check_negative_resource_affinity_node_affinity_consistency(
+            $args->{negative_rules},
+            $args->{node_affinity_rules},
+        );
+    },
+    sub {
+        my ($conflicts, $errors) = @_;
+
+        for my $conflict (@$conflicts) {
+            my ($type, $ruleid) = @$conflict;
+
+            if ($type eq 'negative') {
+                push $errors->{$ruleid}->{resources}->@*,
+                    "two or more resources are restricted to less nodes than"
+                    . " available to the resources";
+            } elsif ($type eq 'node-affinity') {
+                push $errors->{$ruleid}->{resources}->@*,
+                    "at least one resource is in a negative resource affinity"
+                    . " rule and this rule would restrict these to less nodes"
+                    . " than available to the resources";
+            }
+        }
+    },
+);
+
+=head1 INTER-PLUGIN RULE CANONICALIZATION HELPERS
+
+=cut
+
+=head3 create_implicit_positive_resource_affinity_node_affinity_rules(...)
+
+Modifies C<$rules> such that all resources of a positive resource affinity rule,
+defined in C<$positive_rules>, where at least one of their resources is also in
+a node affinity rule, defined in C<$node_affinity_rules>, makes all the other
+positive resource affinity rule's resources also part of the node affinity rule.
+
+This helper assumes that there can only be a single node affinity rule per
+positive resource affinity rule as there is no heuristic yet what should be
+done in the case of multiple node affinity rules.
+
+This also makes it cheaper to infer these implicit constraints later instead of
+propagating that information in each scheduler invocation.
+
+=cut
+
+sub create_implicit_positive_resource_affinity_node_affinity_rules {
+    my ($rules, $positive_rules, $node_affinity_rules) = @_;
+
+    my @conflicts = ();
+
+    while (my ($positiveid, $positive_rule) = each %$positive_rules) {
+        my $found_node_affinity_id;
+        my $positive_resources = $positive_rule->{resources};
+
+        for my $node_affinity_id (keys %$node_affinity_rules) {
+            my $node_affinity_rule = $rules->{ids}->{$node_affinity_id};
+            next if sets_are_disjoint($positive_resources, $node_affinity_rule->{resources});
+
+            # assuming that all $resources have at most one node affinity rule,
+            # take the first found node affinity rule.
+            $node_affinity_rule->{resources}->{$_} = 1 for keys %$positive_resources;
+            last;
+        }
+    }
+}
+
+sub global_canonicalize {
+    my ($class, $rules) = @_;
+
+    my $args = $class->get_check_arguments($rules);
+
+    create_implicit_positive_resource_affinity_node_affinity_rules(
+        $rules,
+        $args->{positive_rules},
+        $args->{node_affinity_rules},
+    );
+}
+
 1;
-- 
2.47.2





More information about the pve-devel mailing list