]> git.proxmox.com Git - pve-manager.git/blame - PVE/NodeConfig.pm
acme plugins: improve API
[pve-manager.git] / PVE / NodeConfig.pm
CommitLineData
c4f78bb7
FG
1package PVE::NodeConfig;
2
3use strict;
4use warnings;
5
6use PVE::CertHelpers;
7use PVE::JSONSchema qw(get_standard_option);
8use PVE::Tools qw(file_get_contents file_set_contents lock_file);
cc442d3e
WL
9use PVE::ACME;
10
11# register up to 20 domain names
12my $MAXDOMAINS = 20;
c4f78bb7
FG
13
14my $node_config_lock = '/var/lock/pvenode.lock';
15
16PVE::JSONSchema::register_format('pve-acme-domain', sub {
17 my ($domain, $noerr) = @_;
18
4dab82e3 19 my $label = qr/[a-z0-9][a-z0-9_-]*/i;
c4f78bb7
FG
20
21 return $domain if $domain =~ /^$label(?:\.$label)+$/;
22 return undef if $noerr;
23 die "value does not look like a valid domain name";
24});
25
26sub config_file {
27 my ($node) = @_;
28
29 return "/etc/pve/nodes/${node}/config";
30}
31
32sub load_config {
33 my ($node) = @_;
34
35 my $filename = config_file($node);
36 my $raw = eval { PVE::Tools::file_get_contents($filename); };
37 return {} if !$raw;
38
39 return parse_node_config($raw);
40}
41
42sub write_config {
43 my ($node, $conf) = @_;
44
45 my $filename = config_file($node);
46
47 my $raw = write_node_config($conf);
48
49 PVE::Tools::file_set_contents($filename, $raw);
50}
51
52sub lock_config {
53 my ($node, $code, @param) = @_;
54
55 my $res = lock_file($node_config_lock, 10, $code, @param);
56
57 die $@ if $@;
58
59 return $res;
60}
61
62my $confdesc = {
63 description => {
64 type => 'string',
65 description => 'Node description/comment.',
66 optional => 1,
67 },
b3d84542
CE
68 wakeonlan => {
69 type => 'string',
70 description => 'MAC address for wake on LAN',
0f615ea9 71 format => 'mac-addr',
b3d84542
CE
72 optional => 1,
73 },
1ff3fef0
TL
74 'startall-onboot-delay' => {
75 description => 'Initial delay in seconds, before starting all the Virtual Guests with on-boot enabled.',
76 type => 'integer',
77 minimum => 0,
78 maximum => 300,
79 default => 0,
80 optional => 1,
81 },
c4f78bb7
FG
82};
83
ab53e139 84my $acme_domain_desc = {
cc442d3e
WL
85 domain => {
86 type => 'string',
87 format => 'pve-acme-domain',
88 format_description => 'domain',
89 description => 'domain for this node\'s ACME certificate',
dcfb9f2c 90 default_key => 1,
cc442d3e
WL
91 },
92 plugin => {
93 type => 'string',
94 format => 'pve-configid',
9d17b945 95 description => 'The ACME plugin ID',
cc442d3e 96 format_description => 'name of the plugin configuration',
9d17b945
FG
97 optional => 1,
98 default => 'standalone',
cc442d3e
WL
99 },
100 alias => {
101 type => 'string',
102 format => 'pve-acme-domain',
103 format_description => 'domain',
104 description => 'Alias for the Domain to verify ACME Challenge over DNS',
105 optional => 1,
106 },
107};
cc442d3e 108
c4f78bb7
FG
109my $acmedesc = {
110 account => get_standard_option('pve-acme-account-name'),
111 domains => {
112 type => 'string',
113 format => 'pve-acme-domain-list',
114 format_description => 'domain[;domain;...]',
115 description => 'List of domains for this node\'s ACME certificate',
cc442d3e 116 optional => 1,
c4f78bb7
FG
117 },
118};
c4f78bb7
FG
119
120$confdesc->{acme} = {
121 type => 'string',
122 description => 'Node specific ACME settings.',
123 format => $acmedesc,
124 optional => 1,
125};
126
cc442d3e 127for my $i (0..$MAXDOMAINS) {
ab53e139 128 $confdesc->{"acmedomain$i"} = {
cc442d3e 129 type => 'string',
ab53e139
FG
130 description => 'ACME domain and validation plugin',
131 format => $acme_domain_desc,
cc442d3e
WL
132 optional => 1,
133 };
134};
135
c4f78bb7
FG
136sub check_type {
137 my ($key, $value) = @_;
138
139 die "unknown setting '$key'\n" if !$confdesc->{$key};
140
141 my $type = $confdesc->{$key}->{type};
142
143 if (!defined($value)) {
144 die "got undefined value\n";
145 }
146
147 if ($value =~ m/[\n\r]/) {
148 die "property contains a line feed\n";
149 }
150
151 if ($type eq 'boolean') {
152 return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
153 return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
154 die "type check ('boolean') failed - got '$value'\n";
155 } elsif ($type eq 'integer') {
156 return int($1) if $value =~ m/^(\d+)$/;
157 die "type check ('integer') failed - got '$value'\n";
158 } elsif ($type eq 'number') {
159 return $value if $value =~ m/^(\d+)(\.\d+)?$/;
160 die "type check ('number') failed - got '$value'\n";
161 } elsif ($type eq 'string') {
162 if (my $fmt = $confdesc->{$key}->{format}) {
163 PVE::JSONSchema::check_format($fmt, $value);
164 return $value;
e79894eb
TL
165 } elsif (my $pattern = $confdesc->{$key}->{pattern}) {
166 if ($value !~ m/^$pattern$/) {
167 die "value does not match the regex pattern\n";
168 }
c4f78bb7
FG
169 }
170 return $value;
171 } else {
172 die "internal error"
173 }
174}
e79894eb 175
c4f78bb7
FG
176sub parse_node_config {
177 my ($content) = @_;
178
179 return undef if !defined($content);
180
181 my $conf = {
182 digest => Digest::SHA::sha1_hex($content),
183 };
184 my $descr = '';
185
186 my @lines = split(/\n/, $content);
187 foreach my $line (@lines) {
e61f1629 188 if ($line =~ /^\#(.*)\s*$/ || $line =~ /^description:\s*(.*\S)\s*$/) {
c4f78bb7
FG
189 $descr .= PVE::Tools::decode_text($1) . "\n";
190 next;
191 }
cc95254e 192 if ($line =~ /^([a-z][a-z-_]*\d*):\s*(\S.*)\s*$/) {
c4f78bb7
FG
193 my $key = $1;
194 my $value = $2;
195 eval { $value = check_type($key, $value); };
196 warn "cannot parse value of '$key' in node config: $@" if $@;
197 $conf->{$key} = $value;
198 } else {
199 warn "cannot parse line '$line' in node config\n";
200 }
201 }
202
203 $conf->{description} = $descr if $descr;
204
205 return $conf;
206}
207
208sub write_node_config {
209 my ($conf) = @_;
210
211 my $raw = '';
212 # add description as comment to top of file
213 my $descr = $conf->{description} || '';
214 foreach my $cl (split(/\n/, $descr)) {
215 $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
216 }
217
218 for my $key (sort keys %$conf) {
219 next if ($key eq 'description');
220 next if ($key eq 'digest');
221
222 my $value = $conf->{$key};
223 die "detected invalid newline inside property '$key'\n"
224 if $value =~ m/\n/;
225 $raw .= "$key: $value\n";
226 }
227
228 return $raw;
229}
230
c30e112e 231sub get_acme_conf {
c4f78bb7
FG
232 my ($data, $noerr) = @_;
233
234 $data //= '';
235
c30e112e
WL
236 my $res = {};
237
238 if (defined($data->{acme})) {
b232807d 239 $res = eval {
c30e112e
WL
240 PVE::JSONSchema::parse_property_string($acmedesc, $data->{acme});
241 };
242 if ($@) {
243 return undef if $noerr;
244 die $@;
245 }
b232807d
FG
246 my $standalone_domains = delete($res->{domains}) // '';
247 foreach my $domain (split(";", $standalone_domains)) {
248 $res->{domains}->{$domain}->{plugin} = 'standalone';
249 }
c4f78bb7 250 }
b232807d
FG
251
252 $res->{account} //= 'default';
c30e112e
WL
253
254 for my $index (0..$MAXDOMAINS) {
ab53e139 255 my $domain_rec = $data->{"acmedomain$index"};
c30e112e
WL
256 next if !defined($domain_rec);
257
b232807d 258 my $parsed = eval {
c30e112e 259 PVE::JSONSchema::parse_property_string(
ab53e139 260 $acme_domain_desc,
c30e112e
WL
261 $domain_rec);
262 };
263 if ($@) {
264 return undef if $noerr;
265 die $@;
266 }
b232807d
FG
267 my $domain = delete $parsed->{domain};
268 if ($res->{domains}->{$domain}) {
269 return undef if $noerr;
270 die "duplicate ACME config for domain '$domain'\n";
271 }
9d17b945 272 $parsed->{plugin} //= 'standalone';
b232807d 273 $res->{domains}->{$domain} = $parsed;
c30e112e
WL
274 }
275
c4f78bb7
FG
276 return $res;
277}
278
c4f78bb7 279sub get_nodeconfig_schema {
cc442d3e 280
c4f78bb7
FG
281 return $confdesc;
282}
283
2841;