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