[pve-devel] [PATCH ha-manager v2 23/26] api: introduce ha rules api endpoints

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


Add CRUD API endpoints for HA rules, which assert whether the given
properties for the rules are valid and will not make the existing rule
set infeasible.

Disallowing changes to the rule set via the API, which would make this
and other rules infeasible, makes it safer for users of the HA Manager
to not disrupt the behavior that other rules already enforce.

This functionality can obviously not safeguard manual changes to the
rules config file itself, but manual changes that result in infeasible
rules will be dropped on the next canonalize(...) call by the HA
Manager anyway with a log message.

The use-location-rules feature flag controls whether location rules are
allowed to be created or modified.

Signed-off-by: Daniel Kral <d.kral at proxmox.com>
---
changes since v1:
    - NEW!

 debian/pve-ha-manager.install |   1 +
 src/PVE/API2/HA/Makefile      |   2 +-
 src/PVE/API2/HA/Rules.pm      | 409 ++++++++++++++++++++++++++++++++++
 src/PVE/HA/Config.pm          |   6 +
 4 files changed, 417 insertions(+), 1 deletion(-)
 create mode 100644 src/PVE/API2/HA/Rules.pm

diff --git a/debian/pve-ha-manager.install b/debian/pve-ha-manager.install
index e83c0de..d273959 100644
--- a/debian/pve-ha-manager.install
+++ b/debian/pve-ha-manager.install
@@ -16,6 +16,7 @@
 /usr/share/man/man8/pve-ha-lrm.8.gz
 /usr/share/perl5/PVE/API2/HA/Groups.pm
 /usr/share/perl5/PVE/API2/HA/Resources.pm
+/usr/share/perl5/PVE/API2/HA/Rules.pm
 /usr/share/perl5/PVE/API2/HA/Status.pm
 /usr/share/perl5/PVE/CLI/ha_manager.pm
 /usr/share/perl5/PVE/HA/CRM.pm
diff --git a/src/PVE/API2/HA/Makefile b/src/PVE/API2/HA/Makefile
index 5686efc..86c1013 100644
--- a/src/PVE/API2/HA/Makefile
+++ b/src/PVE/API2/HA/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Resources.pm Groups.pm Status.pm
+SOURCES=Resources.pm Groups.pm Rules.pm Status.pm
 
 .PHONY: install
 install:
diff --git a/src/PVE/API2/HA/Rules.pm b/src/PVE/API2/HA/Rules.pm
new file mode 100644
index 0000000..e5d6817
--- /dev/null
+++ b/src/PVE/API2/HA/Rules.pm
@@ -0,0 +1,409 @@
+package PVE::API2::HA::Rules;
+
+use strict;
+use warnings;
+
+use HTTP::Status qw(:constants);
+
+use Storable qw(dclone);
+
+use PVE::Cluster qw(cfs_read_file);
+use PVE::Exception;
+use PVE::Tools qw(extract_param);
+use PVE::JSONSchema qw(get_standard_option);
+
+use PVE::HA::Config;
+use PVE::HA::Groups;
+use PVE::HA::Rules;
+use PVE::HA::Rules::Location;
+
+use base qw(PVE::RESTHandler);
+
+my $get_api_ha_rule = sub {
+    my ($rules, $ruleid, $rule_errors) = @_;
+
+    die "no such ha rule '$ruleid'\n" if !$rules->{ids}->{$ruleid};
+
+    my $rule_cfg = dclone($rules->{ids}->{$ruleid});
+
+    $rule_cfg->{rule} = $ruleid;
+    $rule_cfg->{digest} = $rules->{digest};
+    $rule_cfg->{order} = $rules->{order}->{$ruleid};
+
+    # set optional rule parameter's default values
+    PVE::HA::Rules->set_rule_defaults($rule_cfg);
+
+    if ($rule_cfg->{services}) {
+        $rule_cfg->{services} =
+            PVE::HA::Rules->encode_value($rule_cfg->{type}, 'services', $rule_cfg->{services});
+    }
+
+    if ($rule_cfg->{nodes}) {
+        $rule_cfg->{nodes} =
+            PVE::HA::Rules->encode_value($rule_cfg->{type}, 'nodes', $rule_cfg->{nodes});
+    }
+
+    if ($rule_errors) {
+        $rule_cfg->{state} = 'contradictory';
+        $rule_cfg->{errors} = $rule_errors;
+    }
+
+    return $rule_cfg;
+};
+
+my $verify_rule_type_is_allowed = sub {
+    my ($type, $noerr) = @_;
+
+    return 1 if $type ne 'location' || PVE::HA::Config::is_ha_location_enabled();
+
+    die "location rules are disabled in the datacenter config\n" if !$noerr;
+    return 0;
+};
+
+my $assert_services_are_configured = sub {
+    my ($services) = @_;
+
+    my $unconfigured_services = [];
+
+    for my $service (sort keys %$services) {
+        push @$unconfigured_services, $service
+            if !PVE::HA::Config::service_is_configured($service);
+    }
+
+    die "cannot use unmanaged service(s) " . join(', ', @$unconfigured_services) . ".\n"
+        if @$unconfigured_services;
+};
+
+my $assert_nodes_do_exist = sub {
+    my ($nodes) = @_;
+
+    my $nonexistant_nodes = [];
+
+    for my $node (sort keys %$nodes) {
+        push @$nonexistant_nodes, $node
+            if !PVE::Cluster::check_node_exists($node, 1);
+    }
+
+    die "cannot use non-existant node(s) " . join(', ', @$nonexistant_nodes) . ".\n"
+        if @$nonexistant_nodes;
+};
+
+my $check_feasibility = sub {
+    my ($rules) = @_;
+
+    $rules = dclone($rules);
+
+    # set optional rule parameter's default values
+    for my $rule (values %{ $rules->{ids} }) {
+        PVE::HA::Rules->set_rule_defaults($rule);
+    }
+
+    # TODO PVE 10: Remove group migration when HA groups have been fully migrated to location rules
+    if (!PVE::HA::Config::is_ha_location_enabled()) {
+        my $groups = PVE::HA::Config::read_group_config();
+        my $services = PVE::HA::Config::read_and_check_resources_config();
+
+        PVE::HA::Rules::Location::delete_location_rules($rules);
+        PVE::HA::Groups::migrate_groups_to_rules($rules, $groups, $services);
+    }
+
+    return PVE::HA::Rules->check_feasibility($rules);
+};
+
+my $assert_feasibility = sub {
+    my ($rules, $ruleid) = @_;
+
+    my $global_errors = $check_feasibility->($rules);
+    my $rule_errors = $global_errors->{$ruleid};
+
+    return if !$rule_errors;
+
+    # stringify error messages
+    for my $opt (keys %$rule_errors) {
+        $rule_errors->{$opt} = join(', ', @{ $rule_errors->{$opt} });
+    }
+
+    my $param = {
+        code => HTTP_BAD_REQUEST,
+        errors => $rule_errors,
+    };
+
+    my $exc = PVE::Exception->new("Rule '$ruleid' is invalid.\n", %$param);
+
+    my ($pkg, $filename, $line) = caller;
+
+    $exc->{filename} = $filename;
+    $exc->{line} = $line;
+
+    die $exc;
+};
+
+__PACKAGE__->register_method({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    description => "Get HA rules.",
+    permissions => {
+        check => ['perm', '/', ['Sys.Audit']],
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            type => {
+                type => 'string',
+                description => "Limit the returned list to the specified rule type.",
+                enum => PVE::HA::Rules->lookup_types(),
+                optional => 1,
+            },
+            state => {
+                type => 'string',
+                description => "Limit the returned list to the specified rule state.",
+                enum => ['enabled', 'disabled'],
+                optional => 1,
+            },
+            service => {
+                type => 'string',
+                description =>
+                    "Limit the returned list to rules affecting the specified service.",
+                completion => \&PVE::HA::Tools::complete_sid,
+                optional => 1,
+            },
+        },
+    },
+    returns => {
+        type => 'array',
+        items => {
+            type => 'object',
+            properties => {
+                rule => { type => 'string' },
+            },
+            links => [{ rel => 'child', href => '{rule}' }],
+        },
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $type = extract_param($param, 'type');
+        my $state = extract_param($param, 'state');
+        my $service = extract_param($param, 'service');
+
+        my $rules = PVE::HA::Config::read_rules_config();
+
+        my $global_errors = $check_feasibility->($rules);
+
+        my $res = [];
+
+        PVE::HA::Rules::foreach_rule(
+            $rules,
+            sub {
+                my ($rule, $ruleid) = @_;
+
+                my $rule_errors = $global_errors->{$ruleid};
+                my $rule_cfg = $get_api_ha_rule->($rules, $ruleid, $rule_errors);
+
+                # skip rule types which are not allowed
+                return if !$verify_rule_type_is_allowed->($rule_cfg->{type}, 1);
+
+                push @$res, $rule_cfg;
+            },
+            {
+                type => $type,
+                state => $state,
+                sid => $service,
+            },
+        );
+
+        return $res;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'read_rule',
+    method => 'GET',
+    path => '{rule}',
+    description => "Read HA rule.",
+    permissions => {
+        check => ['perm', '/', ['Sys.Audit']],
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            rule => get_standard_option(
+                'pve-ha-rule-id',
+                { completion => \&PVE::HA::Tools::complete_rule },
+            ),
+        },
+    },
+    returns => {
+        type => 'object',
+        properties => {
+            rule => get_standard_option('pve-ha-rule-id'),
+            type => {
+                type => 'string',
+            },
+        },
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $ruleid = extract_param($param, 'rule');
+
+        my $rules = PVE::HA::Config::read_rules_config();
+
+        my $global_errors = $check_feasibility->($rules);
+        my $rule_errors = $global_errors->{$ruleid};
+
+        return $get_api_ha_rule->($rules, $ruleid, $rule_errors);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'create_rule',
+    method => 'POST',
+    path => '',
+    description => "Create HA rule.",
+    permissions => {
+        check => ['perm', '/', ['Sys.Console']],
+    },
+    protected => 1,
+    parameters => PVE::HA::Rules->createSchema(),
+    returns => {
+        type => 'null',
+    },
+    code => sub {
+        my ($param) = @_;
+
+        PVE::Cluster::check_cfs_quorum();
+        mkdir("/etc/pve/ha");
+
+        my $type = extract_param($param, 'type');
+        my $ruleid = extract_param($param, 'rule');
+
+        my $plugin = PVE::HA::Rules->lookup($type);
+
+        my $opts = $plugin->check_config($ruleid, $param, 1, 1);
+
+        PVE::HA::Config::lock_ha_domain(
+            sub {
+                my $rules = PVE::HA::Config::read_rules_config();
+
+                die "HA rule '$ruleid' already defined\n" if $rules->{ids}->{$ruleid};
+
+                $verify_rule_type_is_allowed->($type);
+                $assert_services_are_configured->($opts->{services});
+                $assert_nodes_do_exist->($opts->{nodes}) if $opts->{nodes};
+
+                my $maxorder = (sort { $a <=> $b } values %{ $rules->{order} })[0] || 0;
+
+                $rules->{order}->{$ruleid} = ++$maxorder;
+                $rules->{ids}->{$ruleid} = $opts;
+
+                $assert_feasibility->($rules, $ruleid);
+
+                PVE::HA::Config::write_rules_config($rules);
+            },
+            "create ha rule failed",
+        );
+
+        return undef;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_rule',
+    method => 'PUT',
+    path => '{rule}',
+    description => "Update HA rule.",
+    permissions => {
+        check => ['perm', '/', ['Sys.Console']],
+    },
+    protected => 1,
+    parameters => PVE::HA::Rules->updateSchema(),
+    returns => {
+        type => 'null',
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $ruleid = extract_param($param, 'rule');
+        my $digest = extract_param($param, 'digest');
+        my $delete = extract_param($param, 'delete');
+
+        if ($delete) {
+            $delete = [PVE::Tools::split_list($delete)];
+        }
+
+        PVE::HA::Config::lock_ha_domain(
+            sub {
+                my $rules = PVE::HA::Config::read_rules_config();
+
+                PVE::SectionConfig::assert_if_modified($rules, $digest);
+
+                my $rule = $rules->{ids}->{$ruleid} || die "HA rule '$ruleid' does not exist\n";
+
+                my $type = $rule->{type};
+                my $plugin = PVE::HA::Rules->lookup($type);
+                my $opts = $plugin->check_config($ruleid, $param, 0, 1);
+
+                $verify_rule_type_is_allowed->($type);
+                $assert_services_are_configured->($opts->{services});
+                $assert_nodes_do_exist->($opts->{nodes}) if $opts->{nodes};
+
+                my $options = $plugin->private()->{options}->{$type};
+                PVE::SectionConfig::delete_from_config($rule, $options, $opts, $delete);
+
+                $rule->{$_} = $opts->{$_} for keys $opts->%*;
+
+                $assert_feasibility->($rules, $ruleid);
+
+                PVE::HA::Config::write_rules_config($rules);
+            },
+            "update HA rules failed",
+        );
+
+        return undef;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'delete_rule',
+    method => 'DELETE',
+    path => '{rule}',
+    description => "Delete HA rule.",
+    permissions => {
+        check => ['perm', '/', ['Sys.Console']],
+    },
+    protected => 1,
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            rule => get_standard_option(
+                'pve-ha-rule-id',
+                { completion => \&PVE::HA::Tools::complete_rule },
+            ),
+        },
+    },
+    returns => {
+        type => 'null',
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $ruleid = extract_param($param, 'rule');
+
+        PVE::HA::Config::lock_ha_domain(
+            sub {
+                my $rules = PVE::HA::Config::read_rules_config();
+
+                delete $rules->{ids}->{$ruleid};
+
+                PVE::HA::Config::write_rules_config($rules);
+            },
+            "delete ha rule failed",
+        );
+
+        return undef;
+    },
+});
+
+1;
diff --git a/src/PVE/HA/Config.pm b/src/PVE/HA/Config.pm
index 3442d31..de0fcec 100644
--- a/src/PVE/HA/Config.pm
+++ b/src/PVE/HA/Config.pm
@@ -427,4 +427,10 @@ sub get_service_status {
     return $status;
 }
 
+sub is_ha_location_enabled {
+    my $datacenter_cfg = eval { cfs_read_file('datacenter.cfg') } // {};
+
+    return $datacenter_cfg->{ha}->{'use-location-rules'};
+}
+
 1;
-- 
2.39.5





More information about the pve-devel mailing list