[pve-devel] [PATCH ha-manager v2 04/26] rules: introduce location rule plugin
Daniel Kral
d.kral at proxmox.com
Fri Jun 20 16:31:16 CEST 2025
Add the location rule plugin to allow users to specify node affinity
constraints for independent services.
Location rules must specify one or more services, one or more node with
optional priorities (the default is 0), and a strictness, which is
either
* 0 (loose): services MUST be located on one of the rules' nodes, or
* 1 (strict): services SHOULD be located on one of the rules' nodes.
The initial implementation restricts location rules to only specify a
single service once in all location rules, else these location rules
will not be applied.
This makes location rules structurally equivalent to HA groups with the
exception of the "failback" option, which will be moved to the service
config in an upcoming patch.
The services property is added to the rules base plugin as it will also
be used by the colocation rule plugin in the next patch.
Signed-off-by: Daniel Kral <d.kral at proxmox.com>
---
changes since v1:
- NEW!
debian/pve-ha-manager.install | 1 +
src/PVE/HA/Makefile | 1 +
src/PVE/HA/Rules.pm | 31 ++++-
src/PVE/HA/Rules/Location.pm | 206 ++++++++++++++++++++++++++++++++++
src/PVE/HA/Rules/Makefile | 6 +
src/PVE/HA/Tools.pm | 24 ++++
6 files changed, 267 insertions(+), 2 deletions(-)
create mode 100644 src/PVE/HA/Rules/Location.pm
create mode 100644 src/PVE/HA/Rules/Makefile
diff --git a/debian/pve-ha-manager.install b/debian/pve-ha-manager.install
index 9bbd375..2835492 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/Location.pm
/usr/share/perl5/PVE/HA/Tools.pm
/usr/share/perl5/PVE/HA/Usage.pm
/usr/share/perl5/PVE/HA/Usage/Basic.pm
diff --git a/src/PVE/HA/Makefile b/src/PVE/HA/Makefile
index 489cbc0..e386cbf 100644
--- a/src/PVE/HA/Makefile
+++ b/src/PVE/HA/Makefile
@@ -8,6 +8,7 @@ install:
install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE/HA
for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/HA/$$i; done
make -C Resources install
+ make -C Rules install
make -C Usage install
make -C Env install
diff --git a/src/PVE/HA/Rules.pm b/src/PVE/HA/Rules.pm
index e1c84bc..4134283 100644
--- a/src/PVE/HA/Rules.pm
+++ b/src/PVE/HA/Rules.pm
@@ -112,6 +112,13 @@ my $defaultData = {
default => 'enabled',
optional => 1,
},
+ services => get_standard_option(
+ 'pve-ha-resource-id-list',
+ {
+ completion => \&PVE::HA::Tools::complete_sid,
+ optional => 0,
+ },
+ ),
comment => {
description => "HA rule description.",
type => 'string',
@@ -148,7 +155,17 @@ sub decode_plugin_value {
sub decode_value {
my ($class, $type, $key, $value) = @_;
- if ($key eq 'comment') {
+ if ($key eq 'services') {
+ my $res = {};
+
+ for my $service (PVE::Tools::split_list($value)) {
+ if (PVE::HA::Tools::pve_verify_ha_resource_id($service)) {
+ $res->{$service} = 1;
+ }
+ }
+
+ return $res;
+ } elsif ($key eq 'comment') {
return PVE::Tools::decode_text($value);
}
@@ -179,7 +196,11 @@ sub encode_plugin_value {
sub encode_value {
my ($class, $type, $key, $value) = @_;
- if ($key eq 'comment') {
+ if ($key eq 'services') {
+ PVE::HA::Tools::pve_verify_ha_resource_id($_) for keys %$value;
+
+ return join(',', sort keys %$value);
+ } elsif ($key eq 'comment') {
return PVE::Tools::encode_text($value);
}
@@ -405,6 +426,10 @@ The filter properties for C<$opts> are:
=over
+=item C<$sid>
+
+Limits C<$rules> to those which contain the given service C<$sid>.
+
=item C<$type>
Limits C<$rules> to those which are of rule type C<$type>.
@@ -420,6 +445,7 @@ Limits C<$rules> to those which are in the rule state C<$state>.
sub foreach_rule : prototype($$;$) {
my ($rules, $func, $opts) = @_;
+ my $sid = $opts->{sid};
my $type = $opts->{type};
my $state = $opts->{state};
@@ -432,6 +458,7 @@ sub foreach_rule : prototype($$;$) {
next if !$rule; # skip invalid rules
next if defined($type) && $rule->{type} ne $type;
+ next if defined($sid) && !defined($rule->{services}->{$sid});
# rules are enabled by default
my $rule_state = defined($rule->{state}) ? $rule->{state} : 'enabled';
diff --git a/src/PVE/HA/Rules/Location.pm b/src/PVE/HA/Rules/Location.pm
new file mode 100644
index 0000000..67f0b32
--- /dev/null
+++ b/src/PVE/HA/Rules/Location.pm
@@ -0,0 +1,206 @@
+package PVE::HA::Rules::Location;
+
+use strict;
+use warnings;
+
+use Storable qw(dclone);
+
+use PVE::Cluster;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools;
+
+use PVE::HA::Rules;
+use PVE::HA::Tools;
+
+use base qw(PVE::HA::Rules);
+
+=head1 NAME
+
+PVE::HA::Rules::Location
+
+=head1 DESCRIPTION
+
+This package provides the capability to specify and apply location rules, which
+put affinity constraints between a set of HA services and a set of nodes.
+
+Location rules can be either C<'loose'> or C<'strict'>:
+
+=over
+
+=item C<'loose'>
+
+Loose location rules SHOULD be applied if possible, i.e., HA services SHOULD
+prefer to be on the defined nodes, but may fall back to other non-defined nodes,
+if none of the defined nodes are available.
+
+=item C<'strict'>
+
+Strict location rules MUST be applied, i.e., HA services MUST prefer to be on
+the defined nodes. In other words, these HA services are restricted to the
+defined nodes and may not run on any other non-defined node.
+
+=back
+
+=cut
+
+sub type {
+ return 'location';
+}
+
+sub properties {
+ return {
+ nodes => get_standard_option(
+ 'pve-ha-group-node-list',
+ {
+ completion => \&PVE::Cluster::get_nodelist,
+ optional => 0,
+ },
+ ),
+ strict => {
+ description => "Describes whether the location rule is mandatory or optional.",
+ verbose_description =>
+ "Describes whether the location rule is mandatory or optional."
+ . "\nA mandatory location rule makes services be restricted to the defined nodes."
+ . " If none of the nodes are available, the service will be stopped."
+ . "\nAn optional location rule makes services prefer to be on the defined nodes."
+ . " If none of the nodes are available, the service may run on any other node.",
+ type => 'boolean',
+ optional => 1,
+ default => 0,
+ },
+ };
+}
+
+sub options {
+ return {
+ services => { optional => 0 },
+ nodes => { optional => 0 },
+ strict => { optional => 1 },
+ state => { optional => 1 },
+ comment => { optional => 1 },
+ };
+}
+
+sub decode_plugin_value {
+ my ($class, $type, $key, $value) = @_;
+
+ if ($key eq 'nodes') {
+ my $res = {};
+
+ for my $node (PVE::Tools::split_list($value)) {
+ if (my ($node, $priority) = PVE::HA::Tools::parse_node_priority($node, 1)) {
+ $res->{$node} = {
+ priority => $priority,
+ };
+ }
+ }
+
+ return $res;
+ }
+
+ return $value;
+}
+
+sub encode_plugin_value {
+ my ($class, $type, $key, $value) = @_;
+
+ if ($key eq 'nodes') {
+ my $res = [];
+
+ for my $node (sort keys %$value) {
+ my $priority = $value->{$node}->{priority};
+
+ if ($priority) {
+ push @$res, "$node:$priority";
+ } else {
+ push @$res, "$node";
+ }
+ }
+
+ return join(',', @$res);
+ }
+
+ return $value;
+}
+
+sub get_plugin_check_arguments {
+ my ($self, $rules) = @_;
+
+ my $result = {
+ location_rules => {},
+ };
+
+ PVE::HA::Rules::foreach_rule(
+ $rules,
+ sub {
+ my ($rule, $ruleid) = @_;
+
+ $result->{location_rules}->{$ruleid} = $rule;
+ },
+ {
+ type => 'location',
+ state => 'enabled',
+ },
+ );
+
+ return $result;
+}
+
+=head1 LOCATION RULE CHECKERS
+
+=cut
+
+=head3 check_singular_service_location_rule($location_rules)
+
+Returns all location rules defined in C<$location_rules> as a list of lists,
+each consisting of the location rule id and the service id, where at least
+one service is shared between them.
+
+If there are none, the returned list is empty.
+
+=cut
+
+sub check_singular_service_location_rule {
+ my ($location_rules) = @_;
+
+ my @conflicts = ();
+ my $located_services = {};
+
+ while (my ($ruleid, $rule) = each %$location_rules) {
+ for my $sid (keys %{ $rule->{services} }) {
+ push @{ $located_services->{$sid} }, $ruleid;
+ }
+ }
+
+ for my $sid (keys %$located_services) {
+ my $ruleids = $located_services->{$sid};
+
+ next if @$ruleids < 2;
+ for my $ruleid (@$ruleids) {
+ push @conflicts, [$ruleid, $sid];
+ }
+ }
+
+ @conflicts = sort { $a->[0] cmp $b->[0] } @conflicts;
+ return \@conflicts;
+}
+
+__PACKAGE__->register_check(
+ sub {
+ my ($args) = @_;
+
+ return check_singular_service_location_rule($args->{location_rules});
+ },
+ sub {
+ my ($conflicts, $errors) = @_;
+
+ for my $conflict (@$conflicts) {
+ my ($ruleid, $sid) = @$conflict;
+
+ push @{ $errors->{$ruleid}->{services} },
+ "service '$sid' is already used in another location rule";
+ }
+ },
+);
+
+1;
diff --git a/src/PVE/HA/Rules/Makefile b/src/PVE/HA/Rules/Makefile
new file mode 100644
index 0000000..e5cf737
--- /dev/null
+++ b/src/PVE/HA/Rules/Makefile
@@ -0,0 +1,6 @@
+SOURCES=Location.pm
+
+.PHONY: install
+install:
+ install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE/HA/Rules
+ for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/HA/Rules/$$i; done
diff --git a/src/PVE/HA/Tools.pm b/src/PVE/HA/Tools.pm
index 767659f..549cbe1 100644
--- a/src/PVE/HA/Tools.pm
+++ b/src/PVE/HA/Tools.pm
@@ -51,6 +51,18 @@ PVE::JSONSchema::register_standard_option(
},
);
+PVE::JSONSchema::register_standard_option(
+ 'pve-ha-resource-id-list',
+ {
+ description =>
+ "List of HA resource IDs. This consists of a list of resource types followed"
+ . " by a resource specific name separated with a colon (example: vm:100,ct:101).",
+ typetext => "<type>:<name>{,<type>:<name>}*",
+ type => 'string',
+ format => 'pve-ha-resource-id-list',
+ },
+);
+
PVE::JSONSchema::register_format('pve-ha-resource-or-vm-id', \&pve_verify_ha_resource_or_vm_id);
sub pve_verify_ha_resource_or_vm_id {
@@ -103,6 +115,18 @@ PVE::JSONSchema::register_standard_option(
},
);
+sub parse_node_priority {
+ my ($value, $noerr) = @_;
+
+ if ($value =~ m/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)(:(\d+))?$/) {
+ # node without priority set defaults to priority 0
+ return ($1, int($4 // 0));
+ }
+
+ return undef if $noerr;
+ die "unable to parse HA node entry '$value'\n";
+}
+
PVE::JSONSchema::register_standard_option(
'pve-ha-group-id',
{
--
2.39.5
More information about the pve-devel
mailing list