From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pmg-devel@lists.proxmox.com
Subject: [pmg-devel] [PATCH api 3/8] add PMG::NodeConfig module
Date: Tue, 9 Mar 2021 15:13:47 +0100 [thread overview]
Message-ID: <20210309141401.19237-4-w.bumiller@proxmox.com> (raw)
In-Reply-To: <20210309141401.19237-1-w.bumiller@proxmox.com>
for node-local configuration, currently only containing acme
domains/account choices
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
src/Makefile | 1 +
src/PMG/NodeConfig.pm | 225 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 226 insertions(+)
create mode 100644 src/PMG/NodeConfig.pm
diff --git a/src/Makefile b/src/Makefile
index c1d4812..ce76f9f 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -117,6 +117,7 @@ LIBSOURCES = \
PMG/RuleDB.pm \
${CLI_CLASSES} \
${SERVICE_CLASSES} \
+ PMG/NodeConfig.pm \
PMG/API2/Subscription.pm \
PMG/API2/APT.pm \
PMG/API2/Network.pm \
diff --git a/src/PMG/NodeConfig.pm b/src/PMG/NodeConfig.pm
new file mode 100644
index 0000000..84c2141
--- /dev/null
+++ b/src/PMG/NodeConfig.pm
@@ -0,0 +1,225 @@
+package PMG::NodeConfig;
+
+use strict;
+use warnings;
+
+use Digest::SHA;
+
+use PVE::INotify;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools;
+
+use PMG::API2::ACMEPlugin;
+use PMG::CertHelpers;
+
+# register up to 5 domain names per node for now
+my $MAXDOMAINS = 5;
+
+my $inotify_file_id = 'pmg-node-config.conf';
+my $config_filename = '/etc/pmg/node.conf';
+my $lockfile = "/var/lock/pmg-node-config.lck";
+
+my $acme_domain_desc = {
+ domain => {
+ type => 'string',
+ format => 'pmg-acme-domain',
+ format_description => 'domain',
+ description => 'domain for this node\'s ACME certificate',
+ default_key => 1,
+ },
+ plugin => {
+ type => 'string',
+ format => 'pve-configid',
+ description => 'The ACME plugin ID',
+ format_description => 'name of the plugin configuration',
+ optional => 1,
+ default => 'standalone',
+ },
+ alias => {
+ type => 'string',
+ format => 'pmg-acme-alias',
+ format_description => 'domain',
+ description => 'Alias for the Domain to verify ACME Challenge over DNS',
+ optional => 1,
+ },
+ usage => {
+ type => 'string',
+ format => 'pmg-certificate-type-list',
+ description => 'Whether this domain is used for the API, SMTP or both',
+ },
+};
+
+my $acmedesc = {
+ account => get_standard_option('pmg-acme-account-name'),
+};
+
+my $confdesc = {
+ acme => {
+ type => 'string',
+ description => 'Node specific ACME settings.',
+ format => $acmedesc,
+ optional => 1,
+ },
+ map {(
+ "acmedomain$_" => {
+ type => 'string',
+ description => 'ACME domain and validation plugin',
+ format => $acme_domain_desc,
+ optional => 1,
+ },
+ )} (0..$MAXDOMAINS),
+};
+
+sub acme_config_schema : prototype(;$) {
+ my ($overrides) = @_;
+
+ $overrides //= {};
+
+ return {
+ type => 'object',
+ additionalProperties => 0,
+ properties => {
+ %$confdesc,
+ %$overrides,
+ },
+ }
+}
+
+my $config_schema = acme_config_schema();
+
+# Parse the config's acme property string if it exists.
+#
+# Returns nothing if the entry is not set.
+sub parse_acme : prototype($) {
+ my ($cfg) = @_;
+ my $data = $cfg->{acme};
+ if (defined($data)) {
+ return PVE::JSONSchema::parse_property_string($acmedesc, $data);
+ }
+ return; # empty list otherwise
+}
+
+# Turn the acme object into a property string.
+sub print_acme : prototype($) {
+ my ($acme) = @_;
+ return PVE::JSONSchema::print_property_string($acmedesc, $acme);
+}
+
+# Parse a domain entry from the config.
+sub parse_domain : prototype($) {
+ my ($data) = @_;
+ return PVE::JSONSchema::parse_property_string($acme_domain_desc, $data);
+}
+
+# Turn a domain object into a property string.
+sub print_domain : prototype($) {
+ my ($domain) = @_;
+ return PVE::JSONSchema::print_property_string($acme_domain_desc, $domain);
+}
+
+sub read_pmg_node_config {
+ my ($filename, $fh) = @_;
+ local $/ = undef; # slurp mode
+ my $raw = defined($fh) ? <$fh> : '';
+ my $digest = Digest::SHA::sha1_hex($raw);
+ my $conf = PVE::JSONSchema::parse_config($config_schema, $filename, $raw);
+ $conf->{digest} = $digest;
+ return $conf;
+}
+
+sub write_pmg_node_config {
+ my ($filename, $fh, $cfg) = @_;
+ my $raw = PVE::JSONSchema::dump_config($config_schema, $filename, $cfg);
+ PVE::Tools::safe_print($filename, $fh, $raw);
+}
+
+PVE::INotify::register_file($inotify_file_id, $config_filename,
+ \&read_pmg_node_config,
+ \&write_pmg_node_config,
+ undef,
+ always_call_parser => 1);
+
+sub lock_config {
+ my ($code) = @_;
+ my $p = PVE::Tools::lock_file($lockfile, undef, $code);
+ die $@ if $@;
+ return $p;
+}
+
+sub load_config {
+ # auto-adds the standalone plugin if no config is there for backwards
+ # compatibility, so ALWAYS call the cfs registered parser
+ return PVE::INotify::read_file($inotify_file_id);
+}
+
+sub write_config {
+ my ($self) = @_;
+ return PVE::INotify::write_file($inotify_file_id, $self);
+}
+
+# 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
+# FIXME: Could also be shared between PVE and PMG
+sub get_acme_conf {
+ my ($conf, $noerr) = @_;
+
+ $conf //= {};
+
+ my $res = {};
+ if (defined($conf->{acme})) {
+ $res = eval {
+ PVE::JSONSchema::parse_property_string($acmedesc, $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';
+
+ for my $index (0..$MAXDOMAINS) {
+ my $domain_rec = $conf->{"acmedomain$index"};
+ next if !defined($domain_rec);
+
+ 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 = PMG::API2::ACMEPlugin::load_config();
+ die "plugin '$plugin_id' for domain '$domain' not found!\n"
+ if !$plugins->{ids}->{$plugin_id};
+ }
+
+ $parsed->{_configkey} = "acmedomain$index";
+ $res->{domains}->{$domain} = $parsed;
+ }
+
+ return $res;
+}
+
+1;
--
2.20.1
next prev parent reply other threads:[~2021-03-09 14:14 UTC|newest]
Thread overview: 32+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-03-09 14:13 [pmg-devel] [RFC api/gui/wtk/acme 0/many] Certificates & ACME Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 1/8] depend on libpmg-rs-perl and proxmox-acme Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 2/8] add PMG::CertHelpers module Wolfgang Bumiller
2021-03-11 10:05 ` Dominik Csapak
2021-03-12 13:55 ` Wolfgang Bumiller
2021-03-09 14:13 ` Wolfgang Bumiller [this message]
2021-03-09 14:13 ` [pmg-devel] [PATCH api 4/8] cluster: sync acme/ and acme-plugins.conf Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 5/8] api: add ACME and ACMEPlugin module Wolfgang Bumiller
2021-03-11 10:41 ` Dominik Csapak
2021-03-12 14:10 ` Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 6/8] add certificates api endpoint Wolfgang Bumiller
2021-03-11 11:06 ` Dominik Csapak
2021-03-12 14:51 ` Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 7/8] add node-config api entry points Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 8/8] add acme and cert subcommands to pmgconfig Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH gui] add certificates and acme view Wolfgang Bumiller
2021-03-11 12:35 ` Dominik Csapak
2021-03-09 14:13 ` [pmg-devel] [PATCH acme] add missing 'use PVE::Acme' statement Wolfgang Bumiller
2021-03-12 15:00 ` [pmg-devel] applied: " Thomas Lamprecht
2021-03-09 14:13 ` [pmg-devel] [PATCH widget-toolkit 1/7] Utils: add ACME related utilities Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH widget-toolkit 2/7] add ACME related data models Wolfgang Bumiller
2021-03-11 12:41 ` Dominik Csapak
2021-03-09 14:13 ` [pmg-devel] [PATCH widget-toolkit 3/7] add ACME forms: Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH widget-toolkit 4/7] add certificate panel Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH widget-toolkit 5/7] add ACME account panel Wolfgang Bumiller
2021-03-11 13:51 ` Dominik Csapak
2021-03-11 15:14 ` Thomas Lamprecht
2021-03-11 15:16 ` Dominik Csapak
2021-03-11 15:27 ` Thomas Lamprecht
2021-03-09 14:14 ` [pmg-devel] [PATCH widget-toolkit 6/7] add ACME plugin editing Wolfgang Bumiller
2021-03-09 14:14 ` [pmg-devel] [PATCH widget-toolkit 7/7] add ACME domain editing Wolfgang Bumiller
2021-03-10 12:27 ` [pmg-devel] [RFC api/gui/wtk/acme 0/many] Certificates & ACME Dominik Csapak
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=20210309141401.19237-4-w.bumiller@proxmox.com \
--to=w.bumiller@proxmox.com \
--cc=pmg-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