From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id CCC301FF17A for ; Fri, 4 Jul 2025 20:17:48 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id D7CD03CD43; Fri, 4 Jul 2025 20:17:28 +0200 (CEST) From: Daniel Kral To: pve-devel@lists.proxmox.com Date: Fri, 4 Jul 2025 20:16:53 +0200 Message-Id: <20250704181659.465441-15-d.kral@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250704181659.465441-1-d.kral@proxmox.com> References: <20250704181659.465441-1-d.kral@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.013 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH ha-manager v3 13/15] api: introduce ha rules api endpoints X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" 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. Signed-off-by: Daniel Kral --- debian/pve-ha-manager.install | 1 + src/PVE/API2/HA/Makefile | 2 +- src/PVE/API2/HA/Rules.pm | 391 ++++++++++++++++++++++++++++++++++ 3 files changed, 393 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 7462663..b4eff27 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..2e5e382 --- /dev/null +++ b/src/PVE/API2/HA/Rules.pm @@ -0,0 +1,391 @@ +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 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->{resources}) { + $rule_cfg->{resources} = + PVE::HA::Rules->encode_value($rule_cfg->{type}, 'resources', $rule_cfg->{resources}); + } + + if ($rule_cfg->{nodes}) { + $rule_cfg->{nodes} = + PVE::HA::Rules->encode_value($rule_cfg->{type}, 'nodes', $rule_cfg->{nodes}); + } + + if ($rule_errors) { + $rule_cfg->{errors} = $rule_errors; + } + + return $rule_cfg; +}; + +my $assert_resources_are_configured = sub { + my ($resources) = @_; + + my $unconfigured_resources = []; + + for my $resource (sort keys %$resources) { + push @$unconfigured_resources, $resource + if !PVE::HA::Config::service_is_configured($resource); + } + + die "cannot use unmanaged resource(s) " . join(', ', @$unconfigured_resources) . ".\n" + if @$unconfigured_resources; +}; + +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 $get_full_rules_config = sub { + my ($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 + my $groups = PVE::HA::Config::read_group_config(); + my $resources = PVE::HA::Config::read_and_check_resources_config(); + + PVE::HA::Groups::migrate_groups_to_rules($rules, $groups, $resources); + + return $rules; +}; + +my $check_feasibility = sub { + my ($rules) = @_; + + $rules = dclone($rules); + + $rules = $get_full_rules_config->($rules); + + 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, + }, + resource => { + type => 'string', + description => + "Limit the returned list to rules affecting the specified resource.", + 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 $resource = extract_param($param, 'resource'); + + my $rules = PVE::HA::Config::read_rules_config(); + $rules = $get_full_rules_config->($rules); + + 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); + + push @$res, $rule_cfg; + }, + { + type => $type, + sid => $resource, + }, + ); + + 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(); + $rules = $get_full_rules_config->($rules); + + 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}; + + $assert_resources_are_configured->($opts->{resources}); + $assert_nodes_do_exist->($opts->{nodes}) if $opts->{nodes}; + + $rules->{order}->{$ruleid} = PVE::HA::Rules::get_next_ordinal($rules); + $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); + + $assert_resources_are_configured->($opts->{resources}); + $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; -- 2.39.5 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel