[pve-devel] [PATCH manager] api: rework /etc/hosts API

Leo Nunner l.nunner at proxmox.com
Wed May 31 11:59:18 CEST 2023


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






More information about the pve-devel mailing list