]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/NodeConfig.pm
add PMG::NodeConfig module
[pmg-api.git] / src / PMG / NodeConfig.pm
1 package PMG::NodeConfig;
2
3 use strict;
4 use warnings;
5
6 use Digest::SHA;
7
8 use PVE::INotify;
9 use PVE::JSONSchema qw(get_standard_option);
10 use PVE::Tools;
11
12 use PMG::API2::ACMEPlugin;
13 use PMG::CertHelpers;
14
15 # register up to 5 domain names per node for now
16 my $MAXDOMAINS = 5;
17
18 my $inotify_file_id = 'pmg-node-config.conf';
19 my $config_filename = '/etc/pmg/node.conf';
20 my $lockfile = "/var/lock/pmg-node-config.lck";
21
22 my $acme_domain_desc = {
23 domain => {
24 type => 'string',
25 format => 'pmg-acme-domain',
26 format_description => 'domain',
27 description => 'domain for this node\'s ACME certificate',
28 default_key => 1,
29 },
30 plugin => {
31 type => 'string',
32 format => 'pve-configid',
33 description => 'The ACME plugin ID',
34 format_description => 'name of the plugin configuration',
35 optional => 1,
36 default => 'standalone',
37 },
38 alias => {
39 type => 'string',
40 format => 'pmg-acme-alias',
41 format_description => 'domain',
42 description => 'Alias for the Domain to verify ACME Challenge over DNS',
43 optional => 1,
44 },
45 usage => {
46 type => 'string',
47 format => 'pmg-certificate-type-list',
48 description => 'Whether this domain is used for the API, SMTP or both',
49 },
50 };
51
52 my $acmedesc = {
53 account => get_standard_option('pmg-acme-account-name'),
54 };
55
56 my $confdesc = {
57 acme => {
58 type => 'string',
59 description => 'Node specific ACME settings.',
60 format => $acmedesc,
61 optional => 1,
62 },
63 map {(
64 "acmedomain$_" => {
65 type => 'string',
66 description => 'ACME domain and validation plugin',
67 format => $acme_domain_desc,
68 optional => 1,
69 },
70 )} (0..$MAXDOMAINS),
71 };
72
73 sub acme_config_schema : prototype(;$) {
74 my ($overrides) = @_;
75
76 $overrides //= {};
77
78 return {
79 type => 'object',
80 additionalProperties => 0,
81 properties => {
82 %$confdesc,
83 %$overrides,
84 },
85 }
86 }
87
88 my $config_schema = acme_config_schema();
89
90 # Parse the config's acme property string if it exists.
91 #
92 # Returns nothing if the entry is not set.
93 sub parse_acme : prototype($) {
94 my ($cfg) = @_;
95 my $data = $cfg->{acme};
96 if (defined($data)) {
97 return PVE::JSONSchema::parse_property_string($acmedesc, $data);
98 }
99 return; # empty list otherwise
100 }
101
102 # Turn the acme object into a property string.
103 sub print_acme : prototype($) {
104 my ($acme) = @_;
105 return PVE::JSONSchema::print_property_string($acmedesc, $acme);
106 }
107
108 # Parse a domain entry from the config.
109 sub parse_domain : prototype($) {
110 my ($data) = @_;
111 return PVE::JSONSchema::parse_property_string($acme_domain_desc, $data);
112 }
113
114 # Turn a domain object into a property string.
115 sub print_domain : prototype($) {
116 my ($domain) = @_;
117 return PVE::JSONSchema::print_property_string($acme_domain_desc, $domain);
118 }
119
120 sub read_pmg_node_config {
121 my ($filename, $fh) = @_;
122 local $/ = undef; # slurp mode
123 my $raw = defined($fh) ? <$fh> : '';
124 my $digest = Digest::SHA::sha1_hex($raw);
125 my $conf = PVE::JSONSchema::parse_config($config_schema, $filename, $raw);
126 $conf->{digest} = $digest;
127 return $conf;
128 }
129
130 sub write_pmg_node_config {
131 my ($filename, $fh, $cfg) = @_;
132 my $raw = PVE::JSONSchema::dump_config($config_schema, $filename, $cfg);
133 PVE::Tools::safe_print($filename, $fh, $raw);
134 }
135
136 PVE::INotify::register_file($inotify_file_id, $config_filename,
137 \&read_pmg_node_config,
138 \&write_pmg_node_config,
139 undef,
140 always_call_parser => 1);
141
142 sub lock_config {
143 my ($code) = @_;
144 my $p = PVE::Tools::lock_file($lockfile, undef, $code);
145 die $@ if $@;
146 return $p;
147 }
148
149 sub load_config {
150 # auto-adds the standalone plugin if no config is there for backwards
151 # compatibility, so ALWAYS call the cfs registered parser
152 return PVE::INotify::read_file($inotify_file_id);
153 }
154
155 sub write_config {
156 my ($self) = @_;
157 return PVE::INotify::write_file($inotify_file_id, $self);
158 }
159
160 # we always convert domain values to lower case, since DNS entries are not case
161 # sensitive and ACME implementations might convert the ordered identifiers
162 # to lower case
163 # FIXME: Could also be shared between PVE and PMG
164 sub get_acme_conf {
165 my ($conf, $noerr) = @_;
166
167 $conf //= {};
168
169 my $res = {};
170 if (defined($conf->{acme})) {
171 $res = eval {
172 PVE::JSONSchema::parse_property_string($acmedesc, $conf->{acme})
173 };
174 if (my $err = $@) {
175 return undef if $noerr;
176 die $err;
177 }
178 my $standalone_domains = delete($res->{domains}) // '';
179 $res->{domains} = {};
180 for my $domain (split(";", $standalone_domains)) {
181 $domain = lc($domain);
182 die "duplicate domain '$domain' in ACME config properties\n"
183 if defined($res->{domains}->{$domain});
184
185 $res->{domains}->{$domain}->{plugin} = 'standalone';
186 $res->{domains}->{$domain}->{_configkey} = 'acme';
187 }
188 }
189
190 $res->{account} //= 'default';
191
192 for my $index (0..$MAXDOMAINS) {
193 my $domain_rec = $conf->{"acmedomain$index"};
194 next if !defined($domain_rec);
195
196 my $parsed = eval {
197 PVE::JSONSchema::parse_property_string($acme_domain_desc, $domain_rec)
198 };
199 if (my $err = $@) {
200 return undef if $noerr;
201 die $err;
202 }
203 my $domain = lc(delete $parsed->{domain});
204 if (my $exists = $res->{domains}->{$domain}) {
205 return undef if $noerr;
206 die "duplicate domain '$domain' in ACME config properties"
207 ." 'acmedomain$index' and '$exists->{_configkey}'\n";
208 }
209 $parsed->{plugin} //= 'standalone';
210
211 my $plugin_id = $parsed->{plugin};
212 if ($plugin_id ne 'standalone') {
213 my $plugins = PMG::API2::ACMEPlugin::load_config();
214 die "plugin '$plugin_id' for domain '$domain' not found!\n"
215 if !$plugins->{ids}->{$plugin_id};
216 }
217
218 $parsed->{_configkey} = "acmedomain$index";
219 $res->{domains}->{$domain} = $parsed;
220 }
221
222 return $res;
223 }
224
225 1;