From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id D7FE9C159 for ; Thu, 14 Sep 2023 12:03:55 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id BC7C936F3E for ; Thu, 14 Sep 2023 12:03:55 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Thu, 14 Sep 2023 12:03:53 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id B4E884729D for ; Thu, 14 Sep 2023 12:03:53 +0200 (CEST) From: Leo Nunner To: pve-devel@lists.proxmox.com Date: Thu, 14 Sep 2023 12:03:40 +0200 Message-Id: <20230914100341.122329-4-l.nunner@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20230914100341.122329-1-l.nunner@proxmox.com> References: <20230914100341.122329-1-l.nunner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.238 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 POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_3 0.1 random spam to be learned in bayes 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 WIP manager 1/2] api: endpoints for cluster-wide hosts config 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: , X-List-Received-Date: Thu, 14 Sep 2023 10:03:55 -0000 Adds endpoints for a cluster-wide hosts configuration. The configuration is stored at /etc/pve/hosts, and is synced into the local /etc/hosts on each node. The entries are configured via endpoints that reside under /cluster, and can then be synced to local nodes via an endpoint located under /nodes/{node}. The endpoints on the cluster are as follows: - GET /cluster/hosts List all entries in the hosts config (ip, hosts, comment). - POST /cluster/hosts Create a new entry in the config. - PUT /cluster/hosts Update all cluster nodes with the new hosts config. - GET /cluster/hosts/{ip} Get details for a specific IP in the hosts config. - PUT /cluster/hosts/{ip} Update an existing entry in the hosts config. - DELETE /cluster/hosts/{ip} Delete an existing entry from the hosts config. On the node itself, there is only one new endpoint: - PUT /nodes/{node}/hosts Writes the cluster hosts config into the local /etc/hosts. Syncing the cluster config to /etc/hosts works via section markers, where the PVE-managed section gets demarcated from the rest of the file. Currently, this is always located at the bottom of the file, but this may be configurable in the future (e.g. by making it configurable, whether the cluster-wide entries should be an 'override' or a 'fallback'). Signed-off-by: Leo Nunner --- PVE/API2/Cluster.pm | 6 ++ PVE/API2/Cluster/Hosts.pm | 208 ++++++++++++++++++++++++++++++++++++++ PVE/API2/Cluster/Makefile | 3 +- PVE/API2/Nodes.pm | 39 +++++++ PVE/Hosts.pm | 152 ++++++++++++++++++++++++++++ PVE/Makefile | 1 + 6 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 PVE/API2/Cluster/Hosts.pm create mode 100644 PVE/Hosts.pm diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm index 04387ab48..619ae654f 100644 --- a/PVE/API2/Cluster.pm +++ b/PVE/API2/Cluster.pm @@ -27,6 +27,7 @@ use PVE::API2::Backup; use PVE::API2::Cluster::BackupInfo; use PVE::API2::Cluster::Ceph; use PVE::API2::Cluster::Mapping; +use PVE::API2::Cluster::Hosts; use PVE::API2::Cluster::Jobs; use PVE::API2::Cluster::MetricServer; use PVE::API2::Cluster::Notifications; @@ -110,6 +111,11 @@ if ($have_sdn) { }); } +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Cluster::Hosts", + path => 'hosts', +}); + my $dc_schema = PVE::DataCenterConfig::get_datacenter_schema(); my $dc_properties = { delete => { diff --git a/PVE/API2/Cluster/Hosts.pm b/PVE/API2/Cluster/Hosts.pm new file mode 100644 index 000000000..3b722e763 --- /dev/null +++ b/PVE/API2/Cluster/Hosts.pm @@ -0,0 +1,208 @@ +package PVE::API2::Cluster::Hosts; + +use strict; +use warnings; + +use base qw(PVE::RESTHandler); + +use PVE::Hosts; + +use PVE::Tools; + +__PACKAGE__->register_method ({ + name => 'get_entries', + path => '', + method => 'GET', + description => "List entries in cluster-wide hosts configuration.", + permissions => { user => 'all' }, + parameters => { + additionalProperties => 0, + properties => {}, + }, + returns => { + type => "array", + items => { + type => "object", + properties => { + ip => { type => 'string', format => 'ip' }, + hosts => { + type => "array", + items => { type => 'string' }, + }, + comment => { type => 'string' }, + }, + }, + }, + code => sub { + my ($param) = @_; + + my $conf = PVE::Hosts::cluster_config(); + return [ values $conf->%* ]; + } +}); + +__PACKAGE__->register_method ({ + name => 'get_entry', + path => '{ip}', + method => 'GET', + description => "Get entry from cluster-wide hosts configuration.", + permissions => { user => 'all' }, + parameters => { + additionalProperties => 0, + properties => { + ip => { type => 'string', format => 'ip' }, + }, + }, + returns => { + type => "object", + properties => { + ip => { type => 'string', format => 'ip' }, + hosts => { + type => 'array', + items => { type => 'string' }, + }, + comment => { type => 'string' }, + }, + }, + code => sub { + my ($param) = @_; + + my $conf = PVE::Hosts::cluster_config(); + my $ip = $param->{ip}; + + return $conf->{$ip}; + } +}); + +__PACKAGE__->register_method ({ + name => 'create_entry', + path => '', + method => 'POST', + protected => 1, + description => "Create new cluster-wide hosts entry.", + permissions => { user => 'all' }, + parameters => { + additionalProperties => 0, + properties => { + ip => { type => 'string', format => 'ip' }, + hosts => { type => 'string', optional => 1 }, + comment => { type => 'string', optional => 1 }, + }, + }, + returns => { + type => 'object', + }, + code => sub { + my ($param) = @_; + + my $conf = PVE::Hosts::cluster_config(); + my $ip = $param->{ip}; + my $hostname = $param->{hosts}; + my $comment = $param->{comment}; + + die "Entry for $ip exists already" if $conf->{$ip}; + + $conf->{$ip} = { + ip => $ip, + hosts => $hostname, + comment => $comment, + }; + + PVE::Hosts::write_cluster_config($conf); + return undef; + } +}); + +__PACKAGE__->register_method ({ + name => 'update', + path => '{ip}', + method => 'PUT', + protected => 1, + description => "Cluster-wide hosts configuration.", + permissions => { user => 'all' }, + parameters => { + additionalProperties => 0, + properties => { + ip => { type => 'string', format => 'ip' }, + hosts => { type => 'string' }, + comment => { type => 'string', optional => 1 }, + }, + }, + returns => { + type => 'object', + }, + code => sub { + my ($param) = @_; + + my $conf = PVE::Hosts::cluster_config(); + my $ip = $param->{ip}; + my $hosts = $param->{hosts}; + my $comment = $param->{comment}; + + return if !$conf->{$ip}; + + $conf->{$ip}->{comment} = $comment if $comment; + $conf->{$ip}->{hosts} = $hosts if $hosts; + + PVE::Hosts::write_cluster_config($conf); + + return; + } +}); + +__PACKAGE__->register_method ({ + name => 'delete', + path => '{ip}', + method => 'DELETE', + protected => 1, + description => "Cluster-wide hosts configuration.", + permissions => { user => 'all' }, + parameters => { + additionalProperties => 0, + properties => { + ip => { type => 'string', format => 'ip' }, + }, + }, + returns => { + type => 'object', + }, + code => sub { + my ($param) = @_; + + my $conf = PVE::Hosts::cluster_config(); + my $ip = $param->{ip}; + + return if !$conf->{$ip}; + + delete $conf->{$ip}; + PVE::Hosts::write_cluster_config($conf); + + return undef; + } +}); + +__PACKAGE__->register_method ({ + name => 'apply', + path => '', + method => 'PUT', + protected => 1, + description => "Apply cluster-wide hosts configuration.", + permissions => { user => 'all' }, + parameters => { + additionalProperties => 0, + properties => { + }, + }, + returns => { + type => 'object', + }, + code => sub { + my ($param) = @_; + + PVE::Hosts::update_configs(); + + return undef; + } +}); + +1; diff --git a/PVE/API2/Cluster/Makefile b/PVE/API2/Cluster/Makefile index b109e5cb6..f27644f4d 100644 --- a/PVE/API2/Cluster/Makefile +++ b/PVE/API2/Cluster/Makefile @@ -10,7 +10,8 @@ PERLSOURCE= \ Mapping.pm \ Notifications.pm \ Jobs.pm \ - Ceph.pm + Ceph.pm \ + Hosts.pm all: diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm index 5a148d1d0..36b71c4a6 100644 --- a/PVE/API2/Nodes.pm +++ b/PVE/API2/Nodes.pm @@ -2261,6 +2261,45 @@ __PACKAGE__->register_method ({ return; }}); +__PACKAGE__->register_method ({ + name => 'update_etc_hosts', + path => 'hosts', + method => 'PUT', + proxyto => 'node', + protected => 1, + permissions => { + check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]], + }, + description => "Update /etc/hosts.", + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'string', + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + my $worker = sub { + my $conf = PVE::Hosts::cluster_config(); + + my $data = ""; + for my $entry (sort { $a->{ip} cmp $b->{ip} } values $conf->%*) { + $data .= "$entry->{ip} $entry->{hosts}\n"; + $data =~ s/,/ /g; + } + + PVE::Hosts::write_local_config($data); + }; + return $rpcenv->fork_worker('reloadhosts', undef, $authuser, $worker); + }}); + # bash completion helper sub complete_templet_repo { diff --git a/PVE/Hosts.pm b/PVE/Hosts.pm new file mode 100644 index 000000000..f83bd189a --- /dev/null +++ b/PVE/Hosts.pm @@ -0,0 +1,152 @@ +package PVE::Hosts; + +use strict; +use warnings; + +use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use PVE::Tools; + +my $hosts_cluster = "/etc/pve/hosts"; +my $hosts_local = "/etc/hosts"; + +# Cluster-wide configuration + +sub parse_cluster_hosts { + my ($filename, $raw) = @_; + + $raw = '' if !defined($raw); + + my $conf = {}; + + my $pos = 0; + for my $line (split(/\n/, $raw)) { + my ($content, $comment) = split(/ # /, $line); + my ($ip, $hosts) = split(/\s/, $content); + + my $entry = { + ip => $ip, + hosts => $hosts, + comment => $comment, + }; + + $conf->{$ip} = $entry; + } + + return $conf; +} + +sub write_cluster_hosts { + my ($filename, $cfg) = @_; + my $raw = ''; + + for my $entry (sort { $a->{ip} cmp $b->{ip} } values $cfg->%*) { + my $line = ''; + + $line .= "$entry->{ip} $entry->{hosts}"; + $line .= " # $entry->{comment}" if $entry->{comment}; + $line .= "\n"; + + $raw .= $line; + } + + return $raw; +} + +PVE::Cluster::cfs_register_file('hosts', + \&parse_cluster_hosts, + \&write_cluster_hosts); + +sub cluster_config { + return PVE::Cluster::cfs_read_file('hosts'); +} + +sub write_cluster_config { + my ($conf) = @_; + + PVE::Cluster::cfs_lock_file('hosts', undef, sub { + PVE::Cluster::cfs_write_file('hosts', $conf); + }); + + die $@ if $@; +} + +sub write_local_config { + my ($data) = @_; + + my $head = "# --- BEGIN PVE ---\n"; + my $tail = "# --- END PVE ---"; + $data .= "\n" if $data && $data !~ /\n$/; + + my $old = PVE::Tools::file_get_contents($hosts_local); + my @lines = split(/\n/, $old); + + my ($beg, $end); + foreach my $i (0..(@lines-1)) { + my $line = $lines[$i]; + $beg = $i if !defined($beg) && + $line =~ /^#\s*---\s*BEGIN\s*PVE\s*/; + $end = $i if !defined($end) && defined($beg) && + $line =~ /^#\s*---\s*END\s*PVE\s*/i; + last if defined($beg) && defined($end); + } + + if (defined($beg) && defined($end)) { + # Found a section + if ($data) { + splice @lines, $beg, $end-$beg+1, $head.$data.$tail; + } else { + if ($beg == 0 && $end == (@lines-1)) { + return; + } + splice @lines, $beg, $end-$beg+1; + } + PVE::Tools::file_set_contents($hosts_local, join("\n", @lines) . "\n"); + } elsif ($data) { + # No section found + my $content = join("\n", @lines); + chomp $content; + $content .= "\n"; + $data = $head.$data.$tail; + #PVE::Tools::file_set_contents($hosts_local, $content.$data, $perms); + PVE::Tools::file_set_contents($hosts_local, $content.$data); + } + +} + +my $create_reload_hosts_worker = sub { + my ($nodename) = @_; + + my $upid; + PVE::Tools::run_command(['pvesh', 'set', "/nodes/$nodename/hosts"], outfunc => sub { + my $line = shift; + if ($line =~ /^["']?(UPID:[^\s"']+)["']?$/) { + $upid = $1; + } + }); + my $res = PVE::Tools::upid_decode($upid); + + return $res->{pid}; +}; + +sub update_configs { + my () = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + my $code = sub { + $rpcenv->{type} = 'priv'; # to start tasks in background + PVE::Cluster::check_cfs_quorum(); + my $nodelist = PVE::Cluster::get_nodelist(); + for my $node (@$nodelist) { + my $pid = eval { $create_reload_hosts_worker->($node) }; + warn $@ if $@; + } + + return; + }; + + return $rpcenv->fork_worker('reloadhostsall', undef, $authuser, $code); +} + +1; diff --git a/PVE/Makefile b/PVE/Makefile index 660de4d02..23b6dfe27 100644 --- a/PVE/Makefile +++ b/PVE/Makefile @@ -10,6 +10,7 @@ PERLSOURCE = \ CertCache.pm \ CertHelpers.pm \ ExtMetric.pm \ + Hosts.pm \ HTTPServer.pm \ Jobs.pm \ NodeConfig.pm \ -- 2.39.2