[pve-devel] [PATCH WIP manager 1/2] api: endpoints for cluster-wide hosts config

Leo Nunner l.nunner at proxmox.com
Thu Sep 14 12:03:40 CEST 2023


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 <l.nunner at proxmox.com>
---
 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






More information about the pve-devel mailing list