[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