X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FNodeConfig.pm;h=941e6009e8cbd9b681c3f69f341bcf30be8b7ed0;hb=9f65a584b7781b7bb7ed6571513110f5ab0a18e8;hp=560da11667738b226bbd44725b8e5790428a4bcc;hpb=cc442d3ee25c0896864d12dfa8586604ff014ab3;p=pve-manager.git diff --git a/PVE/NodeConfig.pm b/PVE/NodeConfig.pm index 560da116..941e6009 100644 --- a/PVE/NodeConfig.pm +++ b/PVE/NodeConfig.pm @@ -8,8 +8,10 @@ use PVE::JSONSchema qw(get_standard_option); use PVE::Tools qw(file_get_contents file_set_contents lock_file); use PVE::ACME; -# register up to 20 domain names -my $MAXDOMAINS = 20; +use PVE::API2::ACMEPlugin; + +# register up to 5 domain names per node for now +my $MAXDOMAINS = 5; my $node_config_lock = '/var/lock/pvenode.lock'; @@ -20,7 +22,17 @@ PVE::JSONSchema::register_format('pve-acme-domain', sub { return $domain if $domain =~ /^$label(?:\.$label)+$/; return undef if $noerr; - die "value does not look like a valid domain name"; + die "value '$domain' does not look like a valid domain name!\n"; +}); + +PVE::JSONSchema::register_format('pve-acme-alias', sub { + my ($alias, $noerr) = @_; + + my $label = qr/[a-z0-9_][a-z0-9_-]*/i; + + return $alias if $alias =~ /^$label(?:\.$label)+$/; + return undef if $noerr; + die "value '$alias' does not look like a valid alias name!\n"; }); sub config_file { @@ -36,7 +48,7 @@ sub load_config { my $raw = eval { PVE::Tools::file_get_contents($filename); }; return {} if !$raw; - return parse_node_config($raw); + return parse_node_config($raw, $filename); } sub write_config { @@ -50,7 +62,13 @@ sub write_config { } sub lock_config { - my ($node, $code, @param) = @_; + my ($node, $realcode, @param) = @_; + + # make sure configuration file is up-to-date + my $code = sub { + PVE::Cluster::cfs_update(); + $realcode->(@_); + }; my $res = lock_file($node_config_lock, 10, $code, @param); @@ -62,7 +80,9 @@ sub lock_config { my $confdesc = { description => { type => 'string', - description => 'Node description/comment.', + description => "Description for the Node. Shown in the web-interface node notes panel." + ." This is saved as comment inside the configuration file.", + maxLength => 64 * 1024, optional => 1, }, wakeonlan => { @@ -81,28 +101,30 @@ my $confdesc = { }, }; -my $acme_additional_desc = { +my $acme_domain_desc = { domain => { type => 'string', format => 'pve-acme-domain', format_description => 'domain', description => 'domain for this node\'s ACME certificate', + default_key => 1, }, plugin => { type => 'string', format => 'pve-configid', - description => 'The plugin ID, default is standalone http', + description => 'The ACME plugin ID', format_description => 'name of the plugin configuration', + optional => 1, + default => 'standalone', }, alias => { type => 'string', - format => 'pve-acme-domain', + format => 'pve-acme-alias', format_description => 'domain', description => 'Alias for the Domain to verify ACME Challenge over DNS', optional => 1, }, }; -PVE::JSONSchema::register_format('pve-acme-additional-node-conf', $acme_additional_desc); my $acmedesc = { account => get_standard_option('pve-acme-account-name'), @@ -114,7 +136,6 @@ my $acmedesc = { optional => 1, }, }; -PVE::JSONSchema::register_format('pve-acme-node-conf', $acmedesc); $confdesc->{acme} = { type => 'string', @@ -124,82 +145,27 @@ $confdesc->{acme} = { }; for my $i (0..$MAXDOMAINS) { - $confdesc->{"acme_additional_domain$i"} = { + $confdesc->{"acmedomain$i"} = { type => 'string', - description => 'ACME additional Domain', - format => $acme_additional_desc, + description => 'ACME domain and validation plugin', + format => $acme_domain_desc, 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; - } elsif (my $pattern = $confdesc->{$key}->{pattern}) { - if ($value !~ m/^$pattern$/) { - die "value does not match the regex pattern\n"; - } - } - return $value; - } else { - die "internal error" - } -} +my $conf_schema = { + type => 'object', + properties => $confdesc, +}; -sub parse_node_config { - my ($content) = @_; +sub parse_node_config : prototype($$) { + my ($content, $filename) = @_; return undef if !defined($content); + my $digest = Digest::SHA::sha1_hex($content); - my $conf = { - digest => Digest::SHA::sha1_hex($content), - }; - my $descr = ''; - - my @lines = split(/\n/, $content); - foreach my $line (@lines) { - if ($line =~ /^\#(.*)\s*$/ || $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; + my $conf = PVE::JSONSchema::parse_config($conf_schema, $filename, $content, 'description'); + $conf->{digest} = $digest; return $conf; } @@ -227,31 +193,84 @@ sub write_node_config { return $raw; } -sub parse_acme { - my ($data, $noerr) = @_; +# we always convert domain values to lower case, since DNS entries are not case +# sensitive and ACME implementations might convert the ordered identifiers +# to lower case +sub get_acme_conf { + my ($node_conf, $noerr) = @_; + + $node_conf //= {}; + + my $res = {}; + if (defined($node_conf->{acme})) { + $res = eval { + PVE::JSONSchema::parse_property_string($acmedesc, $node_conf->{acme}) + }; + if (my $err = $@) { + return undef if $noerr; + die $err; + } + my $standalone_domains = delete($res->{domains}) // ''; + $res->{domains} = {}; + for my $domain (split(";", $standalone_domains)) { + $domain = lc($domain); + die "duplicate domain '$domain' in ACME config properties\n" + if defined($res->{domains}->{$domain}); + + $res->{domains}->{$domain}->{plugin} = 'standalone'; + $res->{domains}->{$domain}->{_configkey} = 'acme'; + } + } + + $res->{account} //= 'default'; - $data //= ''; + for my $index (0..$MAXDOMAINS) { + my $domain_rec = $node_conf->{"acmedomain$index"}; + next if !defined($domain_rec); - my $res = eval { PVE::JSONSchema::parse_property_string($acmedesc, $data); }; - if ($@) { - return undef if $noerr; - die $@; - } + my $parsed = eval { + PVE::JSONSchema::parse_property_string($acme_domain_desc, $domain_rec) + }; + if (my $err = $@) { + return undef if $noerr; + die $err; + } + my $domain = lc(delete $parsed->{domain}); + if (my $exists = $res->{domains}->{$domain}) { + return undef if $noerr; + die "duplicate domain '$domain' in ACME config properties" + ." 'acmedomain$index' and '$exists->{_configkey}'\n"; + } + $parsed->{plugin} //= 'standalone'; + + my $plugin_id = $parsed->{plugin}; + if ($plugin_id ne 'standalone') { + my $plugins = PVE::API2::ACMEPlugin::load_config(); + die "plugin '$plugin_id' for domain '$domain' not found!\n" + if !$plugins->{ids}->{$plugin_id}; + } - $res->{domains} = [ PVE::Tools::split_list($res->{domains}) ]; + $parsed->{_configkey} = "acmedomain$index"; + $res->{domains}->{$domain} = $parsed; + } return $res; } -sub print_acme { - my ($acme) = @_; +# expects that basic format verification was already done, this is more higher +# level verification +sub verify_conf { + my ($node_conf) = @_; + + # verify ACME domain uniqueness + my $tmp = get_acme_conf($node_conf); - $acme->{domains} = join(';', $acme->{domains}) if $acme->{domains}; - return PVE::JSONSchema::print_property_string($acme, $acmedesc); + # TODO: what else? + + return 1; # OK } sub get_nodeconfig_schema { - return $confdesc; }