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 A3B509C21E for ; Wed, 31 May 2023 11:59:55 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8698037D6C for ; Wed, 31 May 2023 11:59:25 +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 ; Wed, 31 May 2023 11:59:24 +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 4643647E62 for ; Wed, 31 May 2023 11:59:24 +0200 (CEST) From: Leo Nunner To: pve-devel@lists.proxmox.com Date: Wed, 31 May 2023 11:59:18 +0200 Message-Id: <20230531095919.110671-2-l.nunner@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20230531095919.110671-1-l.nunner@proxmox.com> References: <20230531095919.110671-1-l.nunner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.272 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 T_SCC_BODY_TEXT_LINE -0.01 - Subject: [pve-devel] [PATCH manager] api: rework /etc/hosts API 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: Wed, 31 May 2023 09:59:55 -0000 Rework the API abstraction for /etc/nodes, which is located at /nodes/{node} hosts. The endpoints are as follows: (1) GET /nodes/{node}/hosts (2) POST /nodes/{node}/hosts (3) GET /nodes/{node}/hosts/{line} (4) PUT /nodes/{node}/hosts/{line} (5) DELETE /nodes/{node}/hosts/{line} Endpoint (1) provides a full list of all entries inside the /etc/hosts file. They get split up into the following fields: - enabled Whether the line is commented out or not. - line The actual line number inside the file. - ip The IP address which is being mapped. - hosts The list of hostnames for the IP, as a comma-separated list. - value The raw line value as it is stored in /etc/hosts. When "enabled" is set to false (0), the API will still try to parse the value inside the comment. If it's an actual comment (and not just a commented-out entry), "ip" and "hosts" will remain empty, while "value" will contain the comment. There is no way to add new comments via the API. Endpoint (2) adds new entries to /etc/hosts. It takes a line number at which the new entry should be inserted. If there are any entries *after* this line, they get moved down by one position. Endpoints (3), (4) and (5) are all for line based operations. (4) will provide details about a specific line (with the same return values as in (1), while (4) will update an existing line (with new 'enable', 'ip' and 'hosts' values). (5) will delete the specified line, and takes a 'move' parameter which controlls whether the line should simply be replaced with an empty line, or if it should be deleted (which causes all proceeding lines to be moved up by one). Signed-off-by: Leo Nunner --- PVE/API2/Nodes.pm | 298 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 283 insertions(+), 15 deletions(-) diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm index bfe5c40a1..d1e9aca8c 100644 --- a/PVE/API2/Nodes.pm +++ b/PVE/API2/Nodes.pm @@ -2208,24 +2208,71 @@ __PACKAGE__->register_method ({ }, }, returns => { - type => 'object', - properties => { - digest => get_standard_option('pve-config-digest'), - data => { - type => 'string', - description => 'The content of /etc/hosts.' + type => 'array', + items => { + type => 'object', + properties => { + enabled => { + type => 'boolean', + description => 'Enable or disable the entry.', + }, + line => { + type => 'integer', + description => 'Line number of the entry.', + }, + ip => { + type => 'string', + format => 'ip', + description => 'Address to be mapped.', + optional => 1, + }, + hosts => { + type => 'string', + description => 'List of host names.', + optional => 1, + }, + value => { + type => 'string', + description => 'Raw line value.', + }, }, }, }, code => sub { my ($param) = @_; - return PVE::INotify::read_file('etchosts'); + my $ret = []; + my $raw = PVE::INotify::read_file('etchosts'); + + my $pos = -1; + for my $line (split("\n", $raw->{data})) { + $pos++; + next if $line =~ m/^\s*$/; # whitespace/empty lines + my $entry = { + line => $pos, + value => $line, + digest => $raw->{digest}, + }; + + $entry->{enabled} = ($line !~ m/^\s*#/); + + my ($ip, @names) = split(/\s+/, $line); + $ip =~ s/^#(.+)$/$1/; + + if (PVE::JSONSchema::pve_verify_ip($ip, 1)) { + $entry->{ip} = $ip; + $entry->{hosts} = join(',', @names); + } + + push @$ret, $entry; + } + + return $ret; }}); __PACKAGE__->register_method ({ - name => 'write_etc_hosts', + name => 'add_etc_hosts', path => 'hosts', method => 'POST', proxyto => 'node', @@ -2233,15 +2280,170 @@ __PACKAGE__->register_method ({ permissions => { check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]], }, - description => "Write /etc/hosts.", + description => "Update /etc/hosts.", + properties => { + enabled => { + type => 'boolean', + description => 'Enable or disable the entry.', + }, + line => { + type => 'integer', + description => 'Line number of the entry.', + }, + ip => { + type => 'string', + format => 'ip', + description => 'Address to be mapped.', + optional => 1, + }, + hosts => { + type => 'string', + description => 'List of host names.', + optional => 1, + }, + }, + returns => { + type => 'null', + }, + code => sub { + my ($param) = @_; + + PVE::Tools::lock_file('/var/lock/pve-etchosts.lck', undef, sub { + my $hosts = PVE::INotify::read_file('etchosts'); + + my $out = ""; + my @lines = split("\n", $hosts->{data}); + my $pos = 0; + while ($pos < scalar(@lines) || $pos <= $param->{line}) { + if ($pos == $param->{line}) { + my $hosts_line = $param->{hosts}; + $hosts_line =~ s/,/ /g; + $out .= "$param->{ip} $hosts_line \n"; + } + + $out .= $pos >= scalar(@lines) ? "\n" : "$lines[$pos]\n"; + $pos++; + } + + PVE::INotify::write_file('etchosts', $out); + }); + die $@ if $@; + + return; + }}); + +__PACKAGE__->register_method ({ + name => 'get_etc_hosts_line', + path => 'hosts/{line}', + method => 'GET', + proxyto => 'node', + protected => 1, + permissions => { + check => ['perm', '/', [ 'Sys.Audit' ]], + }, + description => "Get the content of /etc/hosts.", parameters => { additionalProperties => 0, properties => { node => get_standard_option('pve-node'), + line => { + type => 'integer', + description => 'Line number.', + }, + }, + }, + returns => { + type => 'object', + properties => { + enabled => { + type => 'boolean', + description => 'Enable or disable the entry.', + }, + line => { + type => 'integer', + description => 'Line number of the entry.', + }, + ip => { + type => 'string', + format => 'ip', + description => 'Address to be mapped.', + optional => 1, + }, + hosts => { + type => 'string', + description => 'List of host names.', + optional => 1, + }, + value => { + type => 'string', + description => 'Raw line value.', + }, digest => get_standard_option('pve-config-digest'), - data => { + }, + }, + code => sub { + my ($param) = @_; + + my $ret = undef; + my $raw = PVE::INotify::read_file('etchosts'); + + my $pos = -1; + for my $line (split("\n", $raw->{data})) { + $pos++; + next if $pos != $param->{line}; + + $ret = { + line => $pos, + value => $line, + digest => $raw->{digest}, + }; + + $ret->{enabled} = ($line !~ m/^\s*#/); + + my ($ip, @names) = split(/\s+/, $line); + $ip =~ s/^#(.+)$/$1/; + + if (PVE::JSONSchema::pve_verify_ip($ip, 1)) { + $ret->{ip} = $ip; + $ret->{hosts} = join(',', @names); + } + } + + return $ret; + }}); + +__PACKAGE__->register_method ({ + name => 'update_etc_hosts', + path => 'hosts/{line}', + method => 'PUT', + proxyto => 'node', + protected => 1, + permissions => { + check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]], + }, + description => "Update /etc/hosts entry.", + parameters => { + type => 'object', + properties => { + digest => get_standard_option('pve-config-digest'), + enabled => { + type => 'boolean', + description => 'Entry is enabled', + }, + line => { + type => 'integer', + description => 'Line number.', + }, + ip => { + type => 'string', + format => 'ip', + description => 'Address.', + optional => 1, + }, + hosts => { type => 'string', - description => 'The target content of /etc/hosts.' + description => 'List of host names.', + optional => 1, }, }, }, @@ -2252,11 +2454,77 @@ __PACKAGE__->register_method ({ my ($param) = @_; PVE::Tools::lock_file('/var/lock/pve-etchosts.lck', undef, sub { - if ($param->{digest}) { - my $hosts = PVE::INotify::read_file('etchosts'); - PVE::Tools::assert_if_modified($hosts->{digest}, $param->{digest}); + my $hosts = PVE::INotify::read_file('etchosts'); + PVE::Tools::assert_if_modified($hosts->{digest}, $param->{digest}); + + my $out = ""; + my @lines = split("\n", $hosts->{data}); + my $pos = 0; + while ($pos < scalar(@lines) || $pos <= $param->{line}) { + if ($pos == $param->{line}) { + my $hosts_line = $param->{hosts}; + $hosts_line =~ s/,/ /g; + $out .= ($param->{enabled} ? '' : '#') . "$param->{ip} $hosts_line \n"; + } else { + $out .= $pos >= scalar(@lines) ? "\n" : "$lines[$pos]\n"; + } + $pos++; } - PVE::INotify::write_file('etchosts', $param->{data}); + + PVE::INotify::write_file('etchosts', $out); + }); + die $@ if $@; + + return; + }}); + +__PACKAGE__->register_method ({ + name => 'delete_etc_hosts', + path => 'hosts/{line}', + method => 'DELETE', + proxyto => 'node', + protected => 1, + permissions => { + check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]], + }, + description => "Delete /etc/hosts entry.", + parameters => { + type => 'object', + properties => { + line => { + type => 'integer', + description => 'Line number of the entry.', + optional => 0, + }, + move => { + type => 'boolean', + description => 'Move up all following lines by 1.', + optional => 0, + }, + }, + }, + returns => { + type => 'null', + }, + code => sub { + my ($param) = @_; + + PVE::Tools::lock_file('/var/lock/pve-etchosts.lck', undef, sub { + my $hosts = PVE::INotify::read_file('etchosts'); + + my $out = ""; + my @lines = split("\n", $hosts->{data}); + my $pos = 0; + while ($pos < scalar(@lines) || $pos <= $param->{line}) { + if ($pos == $param->{line}) { + $out .= '\n' if !$param->{move}; + } else { + $out .= $pos >= scalar(@lines) ? "\n" : "$lines[$pos]\n"; + } + $pos++; + } + + PVE::INotify::write_file('etchosts', $out); }); die $@ if $@; -- 2.30.2