[pve-devel] [PATCH ha-manager v2 05/26] rules: introduce colocation rule plugin

Daniel Kral d.kral at proxmox.com
Fri Jun 20 16:31:17 CEST 2025


Add the colocation rule plugin to allow users to specify inter-service
affinity constraints. Colocation rules must specify two or more services
and a colocation affinity. The inter-service affinity of colocation
rules must be either

  * together (positive): keeping services together, or
  * separate (negative): keeping service separate;

The initial implementation restricts colocation rules to need at least
two specified services, as they are ineffective else, and disallows that
the same two or more services are specified in both a positive and a
negative colocation rule, as that is an infeasible rule set.

Furthermore, positive colocation rules whose service sets overlap are
handled as a single positive colocation rule to make it easier to
retrieve the positively colocated services of a service in later
patches.

Signed-off-by: Daniel Kral <d.kral at proxmox.com>
---
changes since v1:
    - added documentation
    - dropped non-strict colocations
    - replaced `foreach_colocation_rule` with `foreach_rule`
    - removed `split_colocation_rules` and moved that logic mainly in
      `get_plugin_check_arguments(...)`
    - use new `register_check(...)` helper instead of implementing
      `check_feasibility(...)` here
    - renamed `check_service_count` to `check_colocation_service_count`
    - replaced `check_positive_transitivity(...)` with
      `$find_disjoint_colocation_rules` and
      `merge_connected_positive_colocation_rules` canonicalization
      helpers (which is more accurate to find mergeable positive
      colocation rules and is now also not called during the feasibility
      check but only when calling canonicalize)
    - renamed `check_inner_consistency` to
      `check_inter_colocation_consistency`
    - renamed some variables in checkers to make them clearer
    - move `check_{positive,negative}_group_consistency` checks to next
      patch which introduces the global checks to the base plugin

 debian/pve-ha-manager.install  |   1 +
 src/PVE/HA/Rules/Colocation.pm | 287 +++++++++++++++++++++++++++++++++
 src/PVE/HA/Rules/Makefile      |   2 +-
 3 files changed, 289 insertions(+), 1 deletion(-)
 create mode 100644 src/PVE/HA/Rules/Colocation.pm

diff --git a/debian/pve-ha-manager.install b/debian/pve-ha-manager.install
index 2835492..e83c0de 100644
--- a/debian/pve-ha-manager.install
+++ b/debian/pve-ha-manager.install
@@ -33,6 +33,7 @@
 /usr/share/perl5/PVE/HA/Resources/PVECT.pm
 /usr/share/perl5/PVE/HA/Resources/PVEVM.pm
 /usr/share/perl5/PVE/HA/Rules.pm
+/usr/share/perl5/PVE/HA/Rules/Colocation.pm
 /usr/share/perl5/PVE/HA/Rules/Location.pm
 /usr/share/perl5/PVE/HA/Tools.pm
 /usr/share/perl5/PVE/HA/Usage.pm
diff --git a/src/PVE/HA/Rules/Colocation.pm b/src/PVE/HA/Rules/Colocation.pm
new file mode 100644
index 0000000..0539eb3
--- /dev/null
+++ b/src/PVE/HA/Rules/Colocation.pm
@@ -0,0 +1,287 @@
+package PVE::HA::Rules::Colocation;
+
+use strict;
+use warnings;
+
+use PVE::HashTools;
+
+use PVE::HA::Rules;
+
+use base qw(PVE::HA::Rules);
+
+=head1 NAME
+
+PVE::HA::Rules::Colocation - Colocation Plugin for HA Rules
+
+=head1 DESCRIPTION
+
+This package provides the capability to specify and apply colocation rules,
+which put affinity constraints between the HA services. A colocation rule has
+one of the two types: positive (C<'together'>) or negative (C<'separate'>).
+
+Positive colocations specify that HA services need to be kept together, while
+negative colocations specify that HA services need to be kept separate.
+
+Colocation rules MUST be applied. That is, if a HA service cannot comply with
+the colocation rule, it is put in recovery or other error-like states, if there
+is no other way to recover them.
+
+=cut
+
+sub type {
+    return 'colocation';
+}
+
+sub properties {
+    return {
+        affinity => {
+            description => "Describes whether the services are supposed to be kept on separate"
+                . " nodes, or are supposed to be kept together on the same node.",
+            type => 'string',
+            enum => ['separate', 'together'],
+            optional => 0,
+        },
+    };
+}
+
+sub options {
+    return {
+        services => { optional => 0 },
+        affinity => { optional => 0 },
+        state => { optional => 1 },
+        comment => { optional => 1 },
+    };
+}
+
+sub get_plugin_check_arguments {
+    my ($self, $rules) = @_;
+
+    my $result = {
+        colocation_rules => {},
+        positive_rules => {},
+        negative_rules => {},
+    };
+
+    PVE::HA::Rules::foreach_rule(
+        $rules,
+        sub {
+            my ($rule, $ruleid) = @_;
+
+            $result->{colocation_rules}->{$ruleid} = $rule;
+
+            $result->{positive_rules}->{$ruleid} = $rule if $rule->{affinity} eq 'together';
+            $result->{negative_rules}->{$ruleid} = $rule if $rule->{affinity} eq 'separate';
+        },
+        {
+            type => 'colocation',
+            state => 'enabled',
+        },
+    );
+
+    return $result;
+}
+
+=head1 COLOCATION RULE CHECKERS
+
+=cut
+
+=head3 check_colocation_services_count($colocation_rules)
+
+Returns a list of colocation rule ids defined in C<$colocation_rules>, which do
+not have enough services defined to be effective colocation rules.
+
+If there are none, the returned list is empty.
+
+=cut
+
+sub check_colocation_services_count {
+    my ($colocation_rules) = @_;
+
+    my @conflicts = ();
+
+    while (my ($ruleid, $rule) = each %$colocation_rules) {
+        push @conflicts, $ruleid if keys %{ $rule->{services} } < 2;
+    }
+
+    @conflicts = sort @conflicts;
+    return \@conflicts;
+}
+
+__PACKAGE__->register_check(
+    sub {
+        my ($args) = @_;
+
+        return check_colocation_services_count($args->{colocation_rules});
+    },
+    sub {
+        my ($ruleids, $errors) = @_;
+
+        for my $ruleid (@$ruleids) {
+            push @{ $errors->{$ruleid}->{services} },
+                "rule is ineffective as there are less than two services";
+        }
+    },
+);
+
+=head3 check_inter_colocation_consistency($positive_rules, $negative_rules)
+
+Returns a list of lists consisting of a positive colocation rule defined in
+C<$positive_rules> and a negative colocation rule id C<$negative_rules>, which
+share at least the same two services among them. This is an impossible
+constraint as the same services cannot be kept together on the same node and
+kept separate on different nodes at the same time.
+
+If there are none, the returned list is empty.
+
+=cut
+
+sub check_inter_colocation_consistency {
+    my ($positive_rules, $negative_rules) = @_;
+
+    my @conflicts = ();
+
+    while (my ($positiveid, $positive) = each %$positive_rules) {
+        my $positive_services = $positive->{services};
+
+        while (my ($negativeid, $negative) = each %$negative_rules) {
+            my $common_services =
+                PVE::HashTools::set_intersect($positive_services, $negative->{services});
+            next if %$common_services < 2;
+
+            push @conflicts, [$positiveid, $negativeid];
+        }
+    }
+
+    @conflicts = sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] } @conflicts;
+    return \@conflicts;
+}
+
+__PACKAGE__->register_check(
+    sub {
+        my ($args) = @_;
+
+        return check_inter_colocation_consistency(
+            $args->{positive_rules}, $args->{negative_rules},
+        );
+    },
+    sub {
+        my ($conflicts, $errors) = @_;
+
+        for my $conflict (@$conflicts) {
+            my ($positiveid, $negativeid) = @$conflict;
+
+            push @{ $errors->{$positiveid}->{services} },
+                "rule shares two or more services with '$negativeid'";
+            push @{ $errors->{$negativeid}->{services} },
+                "rule shares two or more services with '$positiveid'";
+        }
+    },
+);
+
+=head1 COLOCATION RULE CANONICALIZATION HELPERS
+
+=cut
+
+my $sort_by_lowest_service_id = sub {
+    my ($rules) = @_;
+
+    my $lowest_rule_service_id = {};
+    for my $ruleid (keys %$rules) {
+        my @rule_services = sort keys $rules->{$ruleid}->{services}->%*;
+        $lowest_rule_service_id->{$ruleid} = $rule_services[0];
+    }
+
+    # sort rules such that rules with the lowest numbered service come first
+    my @sorted_ruleids = sort {
+        $lowest_rule_service_id->{$a} cmp $lowest_rule_service_id->{$b}
+    } keys %$rules;
+
+    return @sorted_ruleids;
+};
+
+# returns a list of hashes, which contain disjoint colocation rules, i.e.,
+# put colocation constraints on disjoint sets of services
+my $find_disjoint_colocation_rules = sub {
+    my ($rules) = @_;
+
+    my @disjoint_rules = ();
+
+    # order needed so that it is easier to check whether there is an overlap
+    my @sorted_ruleids = $sort_by_lowest_service_id->($rules);
+
+    for my $ruleid (@sorted_ruleids) {
+        my $rule = $rules->{$ruleid};
+
+        my $found = 0;
+        for my $entry (@disjoint_rules) {
+            next if PVE::HashTools::sets_are_disjoint($rule->{services}, $entry->{services});
+
+            $found = 1;
+            push @{ $entry->{ruleids} }, $ruleid;
+            $entry->{services}->{$_} = 1 for keys $rule->{services}->%*;
+
+            last;
+        }
+        if (!$found) {
+            push @disjoint_rules,
+                {
+                    ruleids => [$ruleid],
+                    services => { $rule->{services}->%* },
+                };
+        }
+    }
+
+    return @disjoint_rules;
+};
+
+=head3 merge_connected_positive_colocation_rules($rules, $positive_rules)
+
+Modifies C<$rules> to contain only disjoint positive colocation rules among the
+ones defined in C<$positive_rules>, i.e., all positive colocation rules put
+positive colocation constraints on disjoint sets of services.
+
+If two or more positive colocation rules have overlapping service sets, then
+these will be removed from C<$rules> and a new positive colocation rule, where
+the rule id is the dashed concatenation of the rule ids (e.g. C<'$rule1-$rule2'>),
+is inserted in C<$rules>.
+
+This makes it cheaper to find the positively colocated services of a service in
+C<$rules> at a later point in time.
+
+=cut
+
+sub merge_connected_positive_colocation_rules {
+    my ($rules, $positive_rules) = @_;
+
+    my @disjoint_positive_rules = $find_disjoint_colocation_rules->($positive_rules);
+
+    for my $entry (@disjoint_positive_rules) {
+        next if @{ $entry->{ruleids} } < 2;
+
+        my $new_ruleid = join('-', @{ $entry->{ruleids} });
+        my $first_ruleid = @{ $entry->{ruleids} }[0];
+
+        $rules->{ids}->{$new_ruleid} = {
+            type => 'colocation',
+            affinity => 'together',
+            services => $entry->{services},
+            state => 'enabled',
+        };
+        $rules->{order}->{$new_ruleid} = $rules->{order}->{$first_ruleid};
+
+        for my $ruleid (@{ $entry->{ruleids} }) {
+            delete $rules->{ids}->{$ruleid};
+            delete $rules->{order}->{$ruleid};
+        }
+    }
+}
+
+sub plugin_canonicalize {
+    my ($class, $rules) = @_;
+
+    my $args = $class->get_plugin_check_arguments($rules);
+
+    merge_connected_positive_colocation_rules($rules, $args->{positive_rules});
+}
+
+1;
diff --git a/src/PVE/HA/Rules/Makefile b/src/PVE/HA/Rules/Makefile
index e5cf737..e08fd94 100644
--- a/src/PVE/HA/Rules/Makefile
+++ b/src/PVE/HA/Rules/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Location.pm
+SOURCES=Colocation.pm Location.pm
 
 .PHONY: install
 install:
-- 
2.39.5





More information about the pve-devel mailing list