]> git.proxmox.com Git - pve-common.git/blame - src/PVE/SectionConfig.pm
section config: add helper for deleting keys from a entry
[pve-common.git] / src / PVE / SectionConfig.pm
CommitLineData
212b08e8
DM
1package PVE::SectionConfig;
2
3use strict;
4use warnings;
848231af 5
7db4e5d7 6use Carp;
212b08e8 7use Digest::SHA;
848231af 8
212b08e8
DM
9use PVE::Exception qw(raise_param_exc);
10use PVE::JSONSchema qw(get_standard_option);
11
212b08e8
DM
12my $defaultData = {
13 options => {},
14 plugins => {},
15 plugindata => {},
16 propertyList => {},
17};
18
19sub private {
20 die "overwrite me";
21 return $defaultData;
22}
23
24sub register {
25 my ($class) = @_;
26
27 my $type = $class->type();
28 my $pdata = $class->private();
29
ebd2b0ac
DM
30 die "duplicate plugin registration (type = $type)"
31 if defined($pdata->{plugins}->{$type});
32
212b08e8
DM
33 my $plugindata = $class->plugindata();
34 $pdata->{plugindata}->{$type} = $plugindata;
35 $pdata->{plugins}->{$type} = $class;
36}
37
38sub type {
39 die "overwrite me";
40}
41
42sub properties {
43 return {};
44}
45
46sub options {
47 return {};
cca9c864 48}
212b08e8
DM
49
50sub plugindata {
51 return {};
cca9c864 52}
212b08e8
DM
53
54sub createSchema {
9c2e4034 55 my ($class, $skip_type) = @_;
212b08e8
DM
56
57 my $pdata = $class->private();
58 my $propertyList = $pdata->{propertyList};
a3530f55
DM
59 my $plugins = $pdata->{plugins};
60
61 my $props = {};
62
63 my $copy_property = sub {
64 my ($src) = @_;
65
66 my $res = {};
67 foreach my $k (keys %$src) {
68 $res->{$k} = $src->{$k};
69 }
70
71 return $res;
72 };
73
74 foreach my $p (keys %$propertyList) {
9c2e4034
DM
75 next if $skip_type && $p eq 'type';
76
a3530f55
DM
77 if (!$propertyList->{$p}->{optional}) {
78 $props->{$p} = $propertyList->{$p};
79 next;
80 }
81
82 my $required = 1;
83
84 my $copts = $class->options();
85 $required = 0 if defined($copts->{$p}) && $copts->{$p}->{optional};
86
87 foreach my $t (keys %$plugins) {
88 my $opts = $pdata->{options}->{$t} || {};
89 $required = 0 if !defined($opts->{$p}) || $opts->{$p}->{optional};
90 }
91
92 if ($required) {
93 # make a copy, because we modify the optional property
94 my $res = &$copy_property($propertyList->{$p});
95 $res->{optional} = 0;
96 $props->{$p} = $res;
97 } else {
98 $props->{$p} = $propertyList->{$p};
99 }
100 }
212b08e8
DM
101
102 return {
103 type => "object",
104 additionalProperties => 0,
a3530f55 105 properties => $props,
212b08e8
DM
106 };
107}
108
109sub updateSchema {
885641fb 110 my ($class, $single_class) = @_;
212b08e8
DM
111
112 my $pdata = $class->private();
113 my $propertyList = $pdata->{propertyList};
114 my $plugins = $pdata->{plugins};
115
116 my $props = {};
117
885641fb
DM
118 my $filter_type = $class->type() if $single_class;
119
212b08e8
DM
120 foreach my $p (keys %$propertyList) {
121 next if $p eq 'type';
885641fb
DM
122
123 my $copts = $class->options();
124
125 next if defined($filter_type) && !defined($copts->{$p});
126
212b08e8
DM
127 if (!$propertyList->{$p}->{optional}) {
128 $props->{$p} = $propertyList->{$p};
129 next;
130 }
516dfb55
DM
131
132 my $modifyable = 0;
133
0d838035
DM
134 $modifyable = 1 if defined($copts->{$p}) && !$copts->{$p}->{fixed};
135
212b08e8 136 foreach my $t (keys %$plugins) {
516dfb55 137 my $opts = $pdata->{options}->{$t} || {};
212b08e8 138 next if !defined($opts->{$p});
516dfb55 139 $modifyable = 1 if !$opts->{$p}->{fixed};
212b08e8 140 }
516dfb55
DM
141 next if !$modifyable;
142
143 $props->{$p} = $propertyList->{$p};
212b08e8
DM
144 }
145
dc5eae7d 146 $props->{digest} = get_standard_option('pve-config-digest');
212b08e8
DM
147
148 $props->{delete} = {
149 type => 'string', format => 'pve-configid-list',
150 description => "A list of settings you want to delete.",
151 maxLength => 4096,
152 optional => 1,
153 };
154
155 return {
156 type => "object",
157 additionalProperties => 0,
158 properties => $props,
159 };
160}
161
162sub init {
163 my ($class) = @_;
164
165 my $pdata = $class->private();
166
167 foreach my $k (qw(options plugins plugindata propertyList)) {
168 $pdata->{$k} = {} if !$pdata->{$k};
169 }
170
171 my $plugins = $pdata->{plugins};
172 my $propertyList = $pdata->{propertyList};
173
174 foreach my $type (keys %$plugins) {
175 my $props = $plugins->{$type}->properties();
176 foreach my $p (keys %$props) {
b732bea7 177 die "duplicate property '$p'" if defined($propertyList->{$p});
212b08e8
DM
178 my $res = $propertyList->{$p} = {};
179 my $data = $props->{$p};
180 for my $a (keys %$data) {
181 $res->{$a} = $data->{$a};
182 }
183 $res->{optional} = 1;
184 }
185 }
186
187 foreach my $type (keys %$plugins) {
188 my $opts = $plugins->{$type}->options();
189 foreach my $p (keys %$opts) {
190 die "undefined property '$p'" if !$propertyList->{$p};
191 }
192 $pdata->{options}->{$type} = $opts;
193 }
194
195 $propertyList->{type}->{type} = 'string';
08aa41d7 196 $propertyList->{type}->{enum} = [sort keys %$plugins];
212b08e8
DM
197}
198
199sub lookup {
200 my ($class, $type) = @_;
201
7db4e5d7
TL
202 croak "cannot lookup undefined type!" if !defined($type);
203
212b08e8
DM
204 my $pdata = $class->private();
205 my $plugin = $pdata->{plugins}->{$type};
206
207 die "unknown section type '$type'\n" if !$plugin;
208
209 return $plugin;
210}
211
212sub lookup_types {
213 my ($class) = @_;
214
215 my $pdata = $class->private();
cca9c864 216
a7f30ebf 217 return [ sort keys %{$pdata->{plugins}} ];
212b08e8
DM
218}
219
220sub decode_value {
221 my ($class, $type, $key, $value) = @_;
222
223 return $value;
224}
225
226sub encode_value {
227 my ($class, $type, $key, $value) = @_;
228
229 return $value;
230}
231
232sub check_value {
233 my ($class, $type, $key, $value, $storeid, $skipSchemaCheck) = @_;
234
235 my $pdata = $class->private();
236
237 return $value if $key eq 'type' && $type eq $value;
238
239 my $opts = $pdata->{options}->{$type};
cca9c864 240 die "unknown section type '$type'\n" if !$opts;
212b08e8
DM
241
242 die "unexpected property '$key'\n" if !defined($opts->{$key});
243
244 my $schema = $pdata->{propertyList}->{$key};
245 die "unknown property type\n" if !$schema;
246
247 my $ct = $schema->{type};
248
249 $value = 1 if $ct eq 'boolean' && !defined($value);
250
251 die "got undefined value\n" if !defined($value);
252
253 die "property contains a line feed\n" if $value =~ m/[\n\r]/;
254
255 if (!$skipSchemaCheck) {
256 my $errors = {};
257 PVE::JSONSchema::check_prop($value, $schema, '', $errors);
258 if (scalar(keys %$errors)) {
259 die "$errors->{$key}\n" if $errors->{$key};
260 die "$errors->{_root}\n" if $errors->{_root};
261 die "unknown error\n";
262 }
263 }
264
bbca3bdc
DM
265 if ($ct eq 'boolean' || $ct eq 'integer' || $ct eq 'number') {
266 return $value + 0; # convert to number
267 }
268
212b08e8
DM
269 return $value;
270}
271
272sub parse_section_header {
273 my ($class, $line) = @_;
274
275 if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
276 my ($type, $sectionId) = ($1, $2);
277 my $errmsg = undef; # set if you want to skip whole section
278 my $config = {}; # to return additional attributes
279 return ($type, $sectionId, $errmsg, $config);
280 }
281 return undef;
282}
283
284sub format_section_header {
2d63b598 285 my ($class, $type, $sectionId, $scfg, $done_hash) = @_;
212b08e8
DM
286
287 return "$type: $sectionId\n";
288}
289
290
291sub parse_config {
c9ede1c3 292 my ($class, $filename, $raw, $allow_unknown) = @_;
212b08e8
DM
293
294 my $pdata = $class->private();
295
296 my $ids = {};
297 my $order = {};
298
3c4d612a
WB
299 $raw = '' if !defined($raw);
300
301 my $digest = Digest::SHA::sha1_hex($raw);
cca9c864 302
212b08e8
DM
303 my $pri = 1;
304
305 my $lineno = 0;
3c4d612a
WB
306 my @lines = split(/\n/, $raw);
307 my $nextline = sub {
e1fbb779 308 while (defined(my $line = shift @lines)) {
3c4d612a 309 $lineno++;
e1fbb779 310 return $line if ($line !~ /^\s*#/);
3c4d612a
WB
311 }
312 };
212b08e8 313
4b32ef6e 314 my $errors = [];
e1fbb779
FE
315 while (@lines) {
316 my $line = $nextline->();
317 next if !$line;
318
212b08e8
DM
319 my $errprefix = "file $filename line $lineno";
320
321 my ($type, $sectionId, $errmsg, $config) = $class->parse_section_header($line);
322 if ($config) {
c9ede1c3
WB
323 my $skip = 0;
324 my $unknown = 0;
212b08e8
DM
325
326 my $plugin;
327
328 if ($errmsg) {
c9ede1c3 329 $skip = 1;
212b08e8
DM
330 chomp $errmsg;
331 warn "$errprefix (skip section '$sectionId'): $errmsg\n";
332 } elsif (!$type) {
c9ede1c3 333 $skip = 1;
212b08e8
DM
334 warn "$errprefix (skip section '$sectionId'): missing type - internal error\n";
335 } else {
336 if (!($plugin = $pdata->{plugins}->{$type})) {
c9ede1c3
WB
337 if ($allow_unknown) {
338 $unknown = 1;
339 } else {
340 $skip = 1;
341 warn "$errprefix (skip section '$sectionId'): unsupported type '$type'\n";
342 }
212b08e8
DM
343 }
344 }
345
9e594bd4 346 while ($line = $nextline->()) {
c9ede1c3 347 next if $skip; # skip
212b08e8
DM
348
349 $errprefix = "file $filename line $lineno";
350
351 if ($line =~ m/^\s+(\S+)(\s+(.*\S))?\s*$/) {
352 my ($k, $v) = ($1, $3);
cca9c864 353
212b08e8
DM
354 eval {
355 die "duplicate attribute\n" if defined($config->{$k});
c9ede1c3
WB
356 if (!$unknown) {
357 $v = $plugin->check_value($type, $k, $v, $sectionId);
358 }
359 $config->{$k} = $v;
212b08e8 360 };
4b32ef6e
FE
361 if (my $err = $@) {
362 warn "$errprefix (section '$sectionId') - unable to parse value of '$k': $err";
363 push @$errors, {
364 context => $errprefix,
365 section => $sectionId,
366 key => $k,
367 err => $err,
368 };
369 }
212b08e8
DM
370
371 } else {
372 warn "$errprefix (section '$sectionId') - ignore config line: $line\n";
373 }
374 }
375
c9ede1c3 376 if ($unknown) {
212b08e8 377 $config->{type} = $type;
c9ede1c3
WB
378 $ids->{$sectionId} = $config;
379 $order->{$sectionId} = $pri++;
380 } elsif (!$skip && $type && $plugin && $config) {
381 $config->{type} = $type;
382 if (!$unknown) {
383 $config = eval { $config = $plugin->check_config($sectionId, $config, 1, 1); };
384 warn "$errprefix (skip section '$sectionId'): $@" if $@;
385 }
386 $ids->{$sectionId} = $config;
212b08e8
DM
387 $order->{$sectionId} = $pri++;
388 }
389
390 } else {
391 warn "$errprefix - ignore config line: $line\n";
392 }
393 }
394
4b32ef6e
FE
395 my $cfg = {
396 ids => $ids,
397 order => $order,
398 digest => $digest
399 };
400 $cfg->{errors} = $errors if scalar(@$errors) > 0;
212b08e8
DM
401
402 return $cfg;
403}
404
405sub check_config {
406 my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
407
408 my $type = $class->type();
409 my $pdata = $class->private();
410 my $opts = $pdata->{options}->{$type};
411
412 my $settings = { type => $type };
413
414 foreach my $k (keys %$config) {
415 my $value = $config->{$k};
d0ba18e1 416
212b08e8 417 die "can't change value of fixed parameter '$k'\n"
95244fd7 418 if !$create && defined($opts->{$k}) && $opts->{$k}->{fixed};
d0ba18e1 419
212b08e8
DM
420 if (defined($value)) {
421 my $tmp = $class->check_value($type, $k, $value, $sectionId, $skipSchemaCheck);
422 $settings->{$k} = $class->decode_value($type, $k, $tmp);
423 } else {
424 die "got undefined value for option '$k'\n";
425 }
426 }
427
428 if ($create) {
429 # check if we have a value for all required options
430 foreach my $k (keys %$opts) {
431 next if $opts->{$k}->{optional};
432 die "missing value for required option '$k'\n"
433 if !defined($config->{$k});
434 }
435 }
436
437 return $settings;
438}
439
440my $format_config_line = sub {
441 my ($schema, $key, $value) = @_;
442
443 my $ct = $schema->{type};
444
0cb4d663
DM
445 die "property '$key' contains a line feed\n"
446 if ($key =~ m/[\n\r]/) || ($value =~ m/[\n\r]/);
447
212b08e8 448 if ($ct eq 'boolean') {
d152f8b0
WB
449 return "\t$key " . ($value ? 1 : 0) . "\n"
450 if defined($value);
212b08e8
DM
451 } else {
452 return "\t$key $value\n" if "$value" ne '';
453 }
454};
455
456sub write_config {
c9ede1c3 457 my ($class, $filename, $cfg, $allow_unknown) = @_;
212b08e8
DM
458
459 my $pdata = $class->private();
460 my $propertyList = $pdata->{propertyList};
461
462 my $out = '';
463
464 my $ids = $cfg->{ids};
465 my $order = $cfg->{order};
466
467 my $maxpri = 0;
468 foreach my $sectionId (keys %$ids) {
cca9c864 469 my $pri = $order->{$sectionId};
212b08e8
DM
470 $maxpri = $pri if $pri && $pri > $maxpri;
471 }
472 foreach my $sectionId (keys %$ids) {
473 if (!defined ($order->{$sectionId})) {
474 $order->{$sectionId} = ++$maxpri;
cca9c864 475 }
212b08e8
DM
476 }
477
478 foreach my $sectionId (sort {$order->{$a} <=> $order->{$b}} keys %$ids) {
479 my $scfg = $ids->{$sectionId};
480 my $type = $scfg->{type};
481 my $opts = $pdata->{options}->{$type};
482
c9ede1c3 483 die "unknown section type '$type'\n" if !$opts && !$allow_unknown;
212b08e8 484
2d63b598
DM
485 my $done_hash = {};
486
487 my $data = $class->format_section_header($type, $sectionId, $scfg, $done_hash);
c9ede1c3
WB
488
489 if (!$opts && $allow_unknown) {
490 $done_hash->{type} = 1;
491 my @first = exists($scfg->{comment}) ? ('comment') : ();
492 for my $k (@first, sort keys %$scfg) {
493 next if defined($done_hash->{$k});
494 $done_hash->{$k} = 1;
495 my $v = $scfg->{$k};
496 $data .= "\t$k $v\n";
497 }
498 $out .= "$data\n";
499 next;
500 }
501
502
2d63b598 503 if ($scfg->{comment} && !$done_hash->{comment}) {
212b08e8
DM
504 my $k = 'comment';
505 my $v = $class->encode_value($type, $k, $scfg->{$k});
506 $data .= &$format_config_line($propertyList->{$k}, $k, $v);
507 }
508
2d63b598 509 $data .= "\tdisable\n" if $scfg->{disable} && !$done_hash->{disable};
212b08e8 510
2d63b598
DM
511 $done_hash->{comment} = 1;
512 $done_hash->{disable} = 1;
212b08e8 513
df89e5eb
DM
514 my @option_keys = sort keys %$opts;
515 foreach my $k (@option_keys) {
2d63b598 516 next if defined($done_hash->{$k});
212b08e8
DM
517 next if $opts->{$k}->{optional};
518 $done_hash->{$k} = 1;
519 my $v = $scfg->{$k};
520 die "section '$sectionId' - missing value for required option '$k'\n"
521 if !defined ($v);
522 $v = $class->encode_value($type, $k, $v);
523 $data .= &$format_config_line($propertyList->{$k}, $k, $v);
524 }
525
df89e5eb 526 foreach my $k (@option_keys) {
212b08e8
DM
527 next if defined($done_hash->{$k});
528 my $v = $scfg->{$k};
529 next if !defined($v);
530 $v = $class->encode_value($type, $k, $v);
531 $data .= &$format_config_line($propertyList->{$k}, $k, $v);
532 }
533
534 $out .= "$data\n";
535 }
536
537 return $out;
538}
539
540sub assert_if_modified {
541 my ($cfg, $digest) = @_;
542
a345b9d5 543 PVE::Tools::assert_if_modified($cfg->{digest}, $digest);
212b08e8
DM
544}
545
5028848d
DC
546sub delete_from_config {
547 my ($config, $option_schema, $new_options, $to_delete) = @_;
548
549 for my $k ($to_delete->@*) {
550 my $d = $option_schema->{$k} || die "no such option '$k'\n";
551 die "unable to delete required option '$k'\n" if !$d->{optional};
552 die "unable to delete fixed option '$k'\n" if $d->{fixed};
553 die "cannot set and delete property '$k' at the same time!\n"
554 if defined($new_options->{$k});
555 delete $config->{$k};
556 }
557
558 return $config;
559}
560
212b08e8 5611;