From ac99650fc2b7e8a2a3503bee156aec78263b680e Mon Sep 17 00:00:00 2001 From: =?utf8?q?Fabian=20Gr=C3=BCnbichler?= Date: Fri, 27 Apr 2018 14:02:09 +0200 Subject: [PATCH] add node configuration file and API MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 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 --- PVE/API2/Makefile | 1 + PVE/API2/NodeConfig.pm | 101 ++++++++++++++++++++ PVE/API2/Nodes.pm | 7 ++ PVE/Makefile | 1 + PVE/NodeConfig.pm | 205 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 315 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/API2/NodeConfig.pm b/PVE/API2/NodeConfig.pm new file mode 100644 index 00000000..fa6925af --- /dev/null +++ b/PVE/API2/NodeConfig.pm @@ -0,0 +1,101 @@ +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' ]], + }, + proxyto => 'node', + 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, + proxyto => 'node', + 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/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/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.39.5