public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Leo Nunner <l.nunner@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH manager] api: rework /etc/hosts API
Date: Wed, 31 May 2023 11:59:18 +0200	[thread overview]
Message-ID: <20230531095919.110671-2-l.nunner@proxmox.com> (raw)
In-Reply-To: <20230531095919.110671-1-l.nunner@proxmox.com>

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@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





  reply	other threads:[~2023-05-31  9:59 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-05-31  9:59 [pve-devel] [PATCH manager widget-toolkit] Abstract /etc/hosts interface Leo Nunner
2023-05-31  9:59 ` Leo Nunner [this message]
2023-05-31  9:59 ` [pve-devel] [PATCH widget-toolkit] introduce abstractions for /etc/hosts view Leo Nunner

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20230531095919.110671-2-l.nunner@proxmox.com \
    --to=l.nunner@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal