[pve-devel] [RFC manager 2/5] add node configuration file and API

Fabian Grünbichler f.gruenbichler at proxmox.com
Wed Apr 11 10:08:48 CEST 2018


this currently only contains a description and the node-specific ACME
configuration, but I am sure we can find other goodies to put there.

Signed-off-by: Fabian Grünbichler <f.gruenbichler at proxmox.com>
---
 PVE/API2/Makefile      |   1 +
 PVE/Makefile           |   1 +
 PVE/API2/NodeConfig.pm |  99 ++++++++++++++++++++++++
 PVE/API2/Nodes.pm      |   7 ++
 PVE/NodeConfig.pm      | 205 +++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 313 insertions(+)
 create mode 100644 PVE/API2/NodeConfig.pm
 create mode 100644 PVE/NodeConfig.pm

diff --git a/PVE/API2/Makefile b/PVE/API2/Makefile
index 86d75d36..51b8b30a 100644
--- a/PVE/API2/Makefile
+++ b/PVE/API2/Makefile
@@ -14,6 +14,7 @@ PERLSOURCE = 			\
 	Pool.pm			\
 	Tasks.pm		\
 	Network.pm		\
+	NodeConfig.pm		\
 	Services.pm
 
 all:
diff --git a/PVE/Makefile b/PVE/Makefile
index 395faf8a..56d27d13 100644
--- a/PVE/Makefile
+++ b/PVE/Makefile
@@ -11,6 +11,7 @@ PERLSOURCE = 			\
 	AutoBalloon.pm		\
 	CephTools.pm		\
 	Report.pm		\
+	NodeConfig.pm		\
 	VZDump.pm
 
 all: pvecfg.pm ${SUBDIRS}
diff --git a/PVE/API2/NodeConfig.pm b/PVE/API2/NodeConfig.pm
new file mode 100644
index 00000000..8c976974
--- /dev/null
+++ b/PVE/API2/NodeConfig.pm
@@ -0,0 +1,99 @@
+package PVE::API2::NodeConfig;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::NodeConfig;
+use PVE::Tools qw(extract_param);
+
+use base qw(PVE::RESTHandler);
+
+my $node_config_schema = PVE::NodeConfig::get_nodeconfig_schema();
+my $node_config_properties = {
+    delete => {
+	type => 'string', format => 'pve-configid-list',
+	description => "A list of settings you want to delete.",
+	optional => 1,
+    },
+    digest => {
+	type => 'string',
+	description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
+	maxLength => 40,
+	optional => 1,
+    },
+    node => get_standard_option('pve-node'),
+};
+
+foreach my $opt (keys %{$node_config_schema}) {
+    $node_config_properties->{$opt} = $node_config_schema->{$opt};
+}
+
+__PACKAGE__->register_method({
+    name => 'get_config',
+    path => '',
+    method => 'GET',
+    description => "Get node configuration options.",
+    permissions => {
+	check => ['perm', '/', [ 'Sys.Audit' ]],
+    },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	},
+    },
+    returns => {
+	type => "object",
+	properties => {},
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::NodeConfig::load_config($param->{node});
+    }});
+
+__PACKAGE__->register_method({
+    name => 'set_options',
+    path => '',
+    method => 'PUT',
+    description => "Set node configuration options.",
+    permissions => {
+	check => ['perm', '/', [ 'Sys.Modify' ]],
+    },
+    protected => 1,
+    parameters => {
+    	additionalProperties => 0,
+	properties => $node_config_properties,
+    },
+    returns => { type => "null" },
+    code => sub {
+	my ($param) = @_;
+
+	my $delete = extract_param($param, 'delete');
+	my $node = extract_param($param, 'node');
+	my $digest = extract_param($param, 'digest');
+
+	my $code = sub {
+	    my $conf = PVE::NodeConfig::load_config($node);
+
+	    PVE::Tools::assert_if_modified($digest, $conf->{digest});
+
+	    foreach my $opt (keys %$param) {
+		$conf->{$opt} = $param->{$opt};
+	    }
+
+	    foreach my $opt (PVE::Tools::split_list($delete)) {
+		delete $conf->{$opt};
+	    };
+
+	    PVE::NodeConfig::write_config($node, $conf);
+	};
+
+	PVE::NodeConfig::lock_config($node, $code);
+	die $@ if $@;
+
+	return undef;
+    }});
+
+1;
diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm
index 3eb38315..42b932cf 100644
--- a/PVE/API2/Nodes.pm
+++ b/PVE/API2/Nodes.pm
@@ -41,6 +41,7 @@ use PVE::API2::APT;
 use PVE::API2::Ceph;
 use PVE::API2::Firewall::Host;
 use PVE::API2::Replication;
+use PVE::API2::NodeConfig;
 use Digest::MD5;
 use Digest::SHA;
 use PVE::API2::Disks;
@@ -118,6 +119,11 @@ __PACKAGE__->register_method ({
     path => 'replication',
 });
 
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::NodeConfig",
+    path => 'config',
+});
+
 __PACKAGE__->register_method ({
     name => 'index', 
     path => '', 
@@ -171,6 +177,7 @@ __PACKAGE__->register_method ({
 	    { name => 'stopall' },
 	    { name => 'netstat' },
 	    { name => 'firewall' },
+	    { name => 'config' },
 	    ];
 
 	return $result;
diff --git a/PVE/NodeConfig.pm b/PVE/NodeConfig.pm
new file mode 100644
index 00000000..33317e02
--- /dev/null
+++ b/PVE/NodeConfig.pm
@@ -0,0 +1,205 @@
+package PVE::NodeConfig;
+
+use strict;
+use warnings;
+
+use PVE::CertHelpers;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools qw(file_get_contents file_set_contents lock_file);
+
+my $node_config_lock = '/var/lock/pvenode.lock';
+
+PVE::JSONSchema::register_format('pve-acme-domain', sub {
+    my ($domain, $noerr) = @_;
+
+    my $label = qr/[a-z][a-z0-9_-]*/i;
+
+    return $domain if $domain =~ /^$label(?:\.$label)+$/;
+    return undef if $noerr;
+    die "value does not look like a valid domain name";
+});
+
+sub config_file {
+    my ($node) = @_;
+
+    return "/etc/pve/nodes/${node}/config";
+}
+
+sub load_config {
+    my ($node) = @_;
+
+    my $filename = config_file($node);
+    my $raw = eval { PVE::Tools::file_get_contents($filename); };
+    return {} if !$raw;
+
+    return parse_node_config($raw);
+}
+
+sub write_config {
+    my ($node, $conf) = @_;
+
+    my $filename = config_file($node);
+
+    my $raw = write_node_config($conf);
+
+    PVE::Tools::file_set_contents($filename, $raw);
+}
+
+sub lock_config {
+    my ($node, $code, @param) = @_;
+
+    my $res = lock_file($node_config_lock, 10, $code, @param);
+
+    die $@ if $@;
+
+    return $res;
+}
+
+my $confdesc = {
+    description => {
+	type => 'string',
+	description => 'Node description/comment.',
+	optional => 1,
+    },
+};
+
+my $acmedesc = {
+    account => get_standard_option('pve-acme-account-name'),
+    domains => {
+	type => 'string',
+	format => 'pve-acme-domain-list',
+	format_description => 'domain[;domain;...]',
+	description => 'List of domains for this node\'s ACME certificate',
+    },
+};
+PVE::JSONSchema::register_format('pve-acme-node-conf', $acmedesc);
+
+$confdesc->{acme} = {
+    type => 'string',
+    description => 'Node specific ACME settings.',
+    format => $acmedesc,
+    optional => 1,
+};
+
+sub check_type {
+    my ($key, $value) = @_;
+
+    die "unknown setting '$key'\n" if !$confdesc->{$key};
+
+    my $type = $confdesc->{$key}->{type};
+
+    if (!defined($value)) {
+	die "got undefined value\n";
+    }
+
+    if ($value =~ m/[\n\r]/) {
+	die "property contains a line feed\n";
+    }
+
+    if ($type eq 'boolean') {
+	return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
+	return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
+	die "type check ('boolean') failed - got '$value'\n";
+    } elsif ($type eq 'integer') {
+	return int($1) if $value =~ m/^(\d+)$/;
+	die "type check ('integer') failed - got '$value'\n";
+    } elsif ($type eq 'number') {
+	return $value if $value =~ m/^(\d+)(\.\d+)?$/;
+	die "type check ('number') failed - got '$value'\n";
+    } elsif ($type eq 'string') {
+	if (my $fmt = $confdesc->{$key}->{format}) {
+	    PVE::JSONSchema::check_format($fmt, $value);
+	    return $value;
+	}
+	return $value;
+    } else {
+	die "internal error"
+    }
+}
+sub parse_node_config {
+    my ($content) = @_;
+
+    return undef if !defined($content);
+
+    my $conf = {
+	digest => Digest::SHA::sha1_hex($content),
+    };
+    my $descr = '';
+
+    my @lines = split(/\n/, $content);
+    foreach my $line (@lines) {
+	if ($line =~ /^\#(.*)\s*$/) {
+	    $descr .= PVE::Tools::decode_text($1) . "\n";
+	    next;
+	}
+	if ($line =~ /^description:\s*(.*\S)\s*$/) {
+	    $descr .= PVE::Tools::decode_text($1) . "\n";
+	    next;
+	}
+	if ($line =~ /^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
+	    my $key = $1;
+	    my $value = $2;
+	    eval { $value = check_type($key, $value); };
+	    warn "cannot parse value of '$key' in node config: $@" if $@;
+	    $conf->{$key} = $value;
+	} else {
+	    warn "cannot parse line '$line' in node config\n";
+	}
+    }
+
+    $conf->{description} = $descr if $descr;
+
+    return $conf;
+}
+
+sub write_node_config {
+    my ($conf) = @_;
+
+    my $raw = '';
+    # add description as comment to top of file
+    my $descr = $conf->{description} || '';
+    foreach my $cl (split(/\n/, $descr)) {
+	$raw .= '#' .  PVE::Tools::encode_text($cl) . "\n";
+    }
+
+    for my $key (sort keys %$conf) {
+	next if ($key eq 'description');
+	next if ($key eq 'digest');
+
+	my $value = $conf->{$key};
+	die "detected invalid newline inside property '$key'\n"
+	    if $value =~ m/\n/;
+	$raw .= "$key: $value\n";
+    }
+
+    return $raw;
+}
+
+sub parse_acme {
+    my ($data, $noerr) = @_;
+
+    $data //= '';
+
+    my $res = eval { PVE::JSONSchema::parse_property_string($acmedesc, $data); };
+    if ($@) {
+	return undef if $noerr;
+	die $@;
+    }
+
+    $res->{domains} = [ PVE::Tools::split_list($res->{domains}) ];
+
+    return $res;
+}
+
+sub print_acme {
+    my ($acme) = @_;
+
+    $acme->{domains} = join(';', $acme->{domains}) if $acme->{domains};
+    return PVE::JSONSchema::print_property_string($acme, $acmedesc);
+}
+
+sub get_nodeconfig_schema {
+    return $confdesc;
+}
+
+1;
-- 
2.14.2





More information about the pve-devel mailing list