]>
Commit | Line | Data |
---|---|---|
212b08e8 DM |
1 | package PVE::SectionConfig; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
848231af | 5 | |
7db4e5d7 | 6 | use Carp; |
212b08e8 | 7 | use Digest::SHA; |
848231af | 8 | |
212b08e8 DM |
9 | use PVE::Exception qw(raise_param_exc); |
10 | use PVE::JSONSchema qw(get_standard_option); | |
7887b1cb DC |
11 | use PVE::Tools; |
12 | ||
13 | # This package provides a way to have multiple (often similar) types of entries | |
14 | # in the same config file, each in its own section, thus "Section Config". | |
15 | # | |
16 | # The intended structure is to have a single 'base' plugin that inherits from | |
17 | # this class and provides meaningful defaults in its '$defaultData', e.g. a | |
18 | # default list of the core properties in its propertyList (most often only 'id' | |
19 | # and 'type') | |
20 | # | |
21 | # Each 'real' plugin then has it's own package that should inherit from the | |
22 | # 'base' plugin and returns it's specific properties in the 'properties' method, | |
23 | # its type in the 'type' method and all the known options, from both parent and | |
24 | # itself, in the 'options' method. | |
25 | # The options method can also be used to define if a property is 'optional' or | |
26 | # 'fixed' (only settable on config entity-creation), for example: | |
27 | # | |
28 | # ```` | |
29 | # sub options { | |
30 | # return { | |
31 | # 'some-optional-property' => { optional => 1 }, | |
32 | # 'a-fixed-property' => { fixed => 1 }, | |
33 | # 'a-required-but-not-fixed-property' => {}, | |
34 | # }; | |
35 | # } | |
36 | # ``` | |
37 | # | |
38 | # 'fixed' options can be set on create, but not changed afterwards. | |
39 | # | |
40 | # To actually use it, you have to first register all the plugins and then init | |
41 | # the 'base' plugin, like so: | |
42 | # | |
43 | # ``` | |
44 | # use PVE::Dummy::Plugin1; | |
45 | # use PVE::Dummy::Plugin2; | |
46 | # use PVE::Dummy::BasePlugin; | |
47 | # | |
48 | # PVE::Dummy::Plugin1->register(); | |
49 | # PVE::Dummy::Plugin2->register(); | |
50 | # PVE::Dummy::BasePlugin->init(); | |
51 | # ``` | |
52 | # | |
53 | # There are two modes for how properties are exposed, the default 'unified' | |
54 | # mode and the 'isolated' mode. | |
55 | # In the default unified mode, there is only a global list of properties | |
56 | # which the plugins can use, so you cannot define the same property name twice | |
57 | # in different plugins. The reason for this is to force the use of identical | |
58 | # properties for multiple plugins. | |
59 | # | |
60 | # The second way is to use the 'isolated' mode, which can be achieved by | |
61 | # calling init with `1` as its parameter like this: | |
62 | # | |
63 | # ``` | |
7ccdc805 | 64 | # PVE::Dummy::BasePlugin->init(property_isolation => 1); |
7887b1cb DC |
65 | # ``` |
66 | # | |
67 | # With this, each plugin get's their own isolated list of properties which it | |
68 | # can use. Note that in this mode, you only have to specify the property in the | |
69 | # options method when it is either 'fixed' or comes from the global list of | |
70 | # properties. All locally defined ones get automatically added to the schema | |
71 | # for that plugin. | |
212b08e8 | 72 | |
212b08e8 DM |
73 | my $defaultData = { |
74 | options => {}, | |
75 | plugins => {}, | |
76 | plugindata => {}, | |
77 | propertyList => {}, | |
78 | }; | |
79 | ||
80 | sub private { | |
81 | die "overwrite me"; | |
82 | return $defaultData; | |
83 | } | |
84 | ||
85 | sub register { | |
86 | my ($class) = @_; | |
87 | ||
88 | my $type = $class->type(); | |
89 | my $pdata = $class->private(); | |
90 | ||
ebd2b0ac DM |
91 | die "duplicate plugin registration (type = $type)" |
92 | if defined($pdata->{plugins}->{$type}); | |
93 | ||
212b08e8 DM |
94 | my $plugindata = $class->plugindata(); |
95 | $pdata->{plugindata}->{$type} = $plugindata; | |
96 | $pdata->{plugins}->{$type} = $class; | |
97 | } | |
98 | ||
99 | sub type { | |
100 | die "overwrite me"; | |
101 | } | |
102 | ||
103 | sub properties { | |
104 | return {}; | |
105 | } | |
106 | ||
107 | sub options { | |
108 | return {}; | |
cca9c864 | 109 | } |
212b08e8 DM |
110 | |
111 | sub plugindata { | |
112 | return {}; | |
cca9c864 | 113 | } |
212b08e8 | 114 | |
7887b1cb DC |
115 | sub has_isolated_properties { |
116 | my ($class) = @_; | |
117 | ||
118 | my $isolatedPropertyList = $class->private()->{isolatedPropertyList}; | |
119 | ||
120 | return defined($isolatedPropertyList) && scalar(keys $isolatedPropertyList->%*) > 0; | |
121 | } | |
122 | ||
123 | my sub compare_property { | |
124 | my ($a, $b, $skip_opts) = @_; | |
125 | ||
126 | my $merged = {$a->%*, $b->%*}; | |
127 | delete $merged->{$_} for $skip_opts->@*; | |
128 | ||
129 | for my $opt (keys $merged->%*) { | |
130 | return 0 if !PVE::Tools::is_deeply($a->{$opt}, $b->{$opt}); | |
131 | } | |
132 | ||
133 | return 1; | |
134 | }; | |
135 | ||
136 | my sub add_property { | |
137 | my ($props, $key, $prop, $type) = @_; | |
138 | ||
139 | if (!defined($props->{$key})) { | |
140 | $props->{$key} = $prop; | |
141 | return; | |
142 | } | |
143 | ||
144 | if (!defined($props->{$key}->{oneOf})) { | |
145 | if (compare_property($props->{$key}, $prop, ['instance-types'])) { | |
146 | push $props->{$key}->{'instance-types'}->@*, $type; | |
147 | } else { | |
148 | my $new_prop = delete $props->{$key}; | |
149 | delete $new_prop->{'type-property'}; | |
150 | delete $prop->{'type-property'}; | |
151 | $props->{$key} = { | |
152 | 'type-property' => 'type', | |
153 | oneOf => [ | |
154 | $new_prop, | |
155 | $prop, | |
156 | ], | |
157 | }; | |
158 | } | |
159 | } else { | |
160 | for my $existing_prop ($props->{$key}->{oneOf}->@*) { | |
161 | if (compare_property($existing_prop, $prop, ['instance-types', 'type-property'])) { | |
162 | push $existing_prop->{'instance-types'}->@*, $type; | |
163 | return; | |
164 | } | |
165 | } | |
166 | ||
167 | push $props->{$key}->{oneOf}->@*, $prop; | |
168 | } | |
169 | }; | |
170 | ||
212b08e8 | 171 | sub createSchema { |
741bf653 | 172 | my ($class, $skip_type, $base) = @_; |
212b08e8 DM |
173 | |
174 | my $pdata = $class->private(); | |
175 | my $propertyList = $pdata->{propertyList}; | |
a3530f55 DM |
176 | my $plugins = $pdata->{plugins}; |
177 | ||
741bf653 | 178 | my $props = $base || {}; |
a3530f55 | 179 | |
7887b1cb DC |
180 | if (!$class->has_isolated_properties()) { |
181 | foreach my $p (keys %$propertyList) { | |
182 | next if $skip_type && $p eq 'type'; | |
a3530f55 | 183 | |
7887b1cb DC |
184 | if (!$propertyList->{$p}->{optional}) { |
185 | $props->{$p} = $propertyList->{$p}; | |
186 | next; | |
187 | } | |
a3530f55 | 188 | |
7887b1cb | 189 | my $required = 1; |
a3530f55 | 190 | |
7887b1cb DC |
191 | my $copts = $class->options(); |
192 | $required = 0 if defined($copts->{$p}) && $copts->{$p}->{optional}; | |
9c2e4034 | 193 | |
7887b1cb DC |
194 | foreach my $t (keys %$plugins) { |
195 | my $opts = $pdata->{options}->{$t} || {}; | |
196 | $required = 0 if !defined($opts->{$p}) || $opts->{$p}->{optional}; | |
197 | } | |
a3530f55 | 198 | |
7887b1cb DC |
199 | if ($required) { |
200 | # make a copy, because we modify the optional property | |
201 | my $res = {$propertyList->{$p}->%*}; # shallow copy | |
202 | $res->{optional} = 0; | |
203 | $props->{$p} = $res; | |
204 | } else { | |
205 | $props->{$p} = $propertyList->{$p}; | |
206 | } | |
a3530f55 | 207 | } |
7887b1cb DC |
208 | } else { |
209 | for my $type (sort keys %$plugins) { | |
210 | my $opts = $pdata->{options}->{$type} || {}; | |
211 | for my $key (sort keys $opts->%*) { | |
212 | my $schema = $class->get_property_schema($type, $key); | |
213 | my $prop = {$schema->%*}; | |
214 | $prop->{'instance-types'} = [$type]; | |
215 | $prop->{'type-property'} = 'type'; | |
216 | $prop->{optional} = 1 if $opts->{$key}->{optional}; | |
217 | ||
218 | add_property($props, $key, $prop, $type); | |
219 | } | |
220 | } | |
221 | # add remaining global properties | |
222 | for my $opt (keys $propertyList->%*) { | |
223 | next if $props->{$opt}; | |
224 | $props->{$opt} = {$propertyList->{$opt}->%*}; | |
225 | } | |
226 | for my $opt (keys $props->%*) { | |
227 | if (my $necessaryTypes = $props->{$opt}->{'instance-types'}) { | |
228 | if ($necessaryTypes->@* == scalar(keys $plugins->%*)) { | |
229 | delete $props->{$opt}->{'instance-types'}; | |
230 | delete $props->{$opt}->{'type-property'}; | |
231 | } else { | |
232 | $props->{$opt}->{optional} = 1; | |
233 | } | |
234 | } | |
a3530f55 DM |
235 | } |
236 | } | |
212b08e8 DM |
237 | |
238 | return { | |
239 | type => "object", | |
240 | additionalProperties => 0, | |
a3530f55 | 241 | properties => $props, |
212b08e8 DM |
242 | }; |
243 | } | |
244 | ||
245 | sub updateSchema { | |
741bf653 | 246 | my ($class, $single_class, $base) = @_; |
212b08e8 DM |
247 | |
248 | my $pdata = $class->private(); | |
249 | my $propertyList = $pdata->{propertyList}; | |
250 | my $plugins = $pdata->{plugins}; | |
251 | ||
741bf653 | 252 | my $props = $base || {}; |
212b08e8 | 253 | |
967e9823 | 254 | my $filter_type = $single_class ? $class->type() : undef; |
885641fb | 255 | |
7887b1cb DC |
256 | if (!$class->has_isolated_properties()) { |
257 | foreach my $p (keys %$propertyList) { | |
258 | next if $p eq 'type'; | |
885641fb | 259 | |
7887b1cb | 260 | my $copts = $class->options(); |
885641fb | 261 | |
7887b1cb | 262 | next if defined($filter_type) && !defined($copts->{$p}); |
885641fb | 263 | |
7887b1cb DC |
264 | if (!$propertyList->{$p}->{optional}) { |
265 | $props->{$p} = $propertyList->{$p}; | |
266 | next; | |
267 | } | |
516dfb55 | 268 | |
7887b1cb | 269 | my $modifyable = 0; |
516dfb55 | 270 | |
7887b1cb | 271 | $modifyable = 1 if defined($copts->{$p}) && !$copts->{$p}->{fixed}; |
0d838035 | 272 | |
7887b1cb DC |
273 | foreach my $t (keys %$plugins) { |
274 | my $opts = $pdata->{options}->{$t} || {}; | |
275 | next if !defined($opts->{$p}); | |
276 | $modifyable = 1 if !$opts->{$p}->{fixed}; | |
277 | } | |
278 | next if !$modifyable; | |
279 | ||
280 | $props->{$p} = $propertyList->{$p}; | |
281 | } | |
282 | } else { | |
283 | for my $type (sort keys %$plugins) { | |
284 | my $opts = $pdata->{options}->{$type} || {}; | |
285 | for my $key (sort keys $opts->%*) { | |
286 | next if $opts->{$key}->{fixed}; | |
287 | ||
288 | my $schema = $class->get_property_schema($type, $key); | |
289 | my $prop = {$schema->%*}; | |
290 | $prop->{'instance-types'} = [$type]; | |
291 | $prop->{'type-property'} = 'type'; | |
292 | $prop->{optional} = 1; | |
293 | ||
294 | add_property($props, $key, $prop, $type); | |
295 | } | |
296 | } | |
297 | ||
298 | for my $opt (keys $propertyList->%*) { | |
299 | next if $props->{$opt}; | |
300 | $props->{$opt} = {$propertyList->{$opt}->%*}; | |
212b08e8 | 301 | } |
516dfb55 | 302 | |
7887b1cb DC |
303 | for my $opt (keys $props->%*) { |
304 | if (my $necessaryTypes = $props->{$opt}->{'instance-types'}) { | |
305 | if ($necessaryTypes->@* == scalar(keys $plugins->%*)) { | |
306 | delete $props->{$opt}->{'instance-types'}; | |
307 | delete $props->{$opt}->{'type-property'}; | |
308 | } | |
309 | } | |
310 | } | |
212b08e8 DM |
311 | } |
312 | ||
dc5eae7d | 313 | $props->{digest} = get_standard_option('pve-config-digest'); |
212b08e8 DM |
314 | |
315 | $props->{delete} = { | |
316 | type => 'string', format => 'pve-configid-list', | |
317 | description => "A list of settings you want to delete.", | |
318 | maxLength => 4096, | |
319 | optional => 1, | |
320 | }; | |
321 | ||
322 | return { | |
323 | type => "object", | |
324 | additionalProperties => 0, | |
325 | properties => $props, | |
326 | }; | |
327 | } | |
328 | ||
7ccdc805 TL |
329 | # the %param hash controls some behavior of the section config, currently the following options are |
330 | # understood: | |
331 | # | |
332 | # - property_isolation: if set, each child-plugin has a fully isolated property (schema) namespace. | |
333 | # By default this is off, meaning all child-plugins share the schema of properties with the same | |
334 | # name. Normally one wants to use oneOf schema's when enabling isolation. | |
212b08e8 | 335 | sub init { |
7ccdc805 TL |
336 | my ($class, %param) = @_; |
337 | ||
338 | my $property_isolation = $param{property_isolation}; | |
212b08e8 DM |
339 | |
340 | my $pdata = $class->private(); | |
341 | ||
7887b1cb | 342 | foreach my $k (qw(options plugins plugindata propertyList isolatedPropertyList)) { |
212b08e8 DM |
343 | $pdata->{$k} = {} if !$pdata->{$k}; |
344 | } | |
345 | ||
346 | my $plugins = $pdata->{plugins}; | |
347 | my $propertyList = $pdata->{propertyList}; | |
7887b1cb | 348 | my $isolatedPropertyList = $pdata->{isolatedPropertyList}; |
212b08e8 DM |
349 | |
350 | foreach my $type (keys %$plugins) { | |
351 | my $props = $plugins->{$type}->properties(); | |
352 | foreach my $p (keys %$props) { | |
7887b1cb DC |
353 | my $res; |
354 | if ($property_isolation) { | |
355 | $res = $isolatedPropertyList->{$type}->{$p} = {}; | |
356 | } else { | |
357 | die "duplicate property '$p'" if defined($propertyList->{$p}); | |
358 | $res = $propertyList->{$p} = {}; | |
359 | } | |
212b08e8 DM |
360 | my $data = $props->{$p}; |
361 | for my $a (keys %$data) { | |
362 | $res->{$a} = $data->{$a}; | |
363 | } | |
364 | $res->{optional} = 1; | |
365 | } | |
366 | } | |
367 | ||
368 | foreach my $type (keys %$plugins) { | |
369 | my $opts = $plugins->{$type}->options(); | |
370 | foreach my $p (keys %$opts) { | |
7887b1cb DC |
371 | my $prop; |
372 | if ($property_isolation) { | |
373 | $prop = $isolatedPropertyList->{$type}->{$p}; | |
374 | } | |
375 | $prop //= $propertyList->{$p}; | |
376 | die "undefined property '$p'" if !$prop; | |
377 | } | |
378 | ||
379 | # automatically the properties to options (if not specified explicitly) | |
380 | if ($property_isolation) { | |
381 | foreach my $p (keys $isolatedPropertyList->{$type}->%*) { | |
382 | next if $opts->{$p}; | |
383 | $opts->{$p} = {}; | |
384 | $opts->{$p}->{optional} = 1 if $isolatedPropertyList->{$type}->{$p}->{optional}; | |
385 | } | |
212b08e8 | 386 | } |
7887b1cb | 387 | |
212b08e8 DM |
388 | $pdata->{options}->{$type} = $opts; |
389 | } | |
390 | ||
391 | $propertyList->{type}->{type} = 'string'; | |
08aa41d7 | 392 | $propertyList->{type}->{enum} = [sort keys %$plugins]; |
212b08e8 DM |
393 | } |
394 | ||
395 | sub lookup { | |
396 | my ($class, $type) = @_; | |
397 | ||
7db4e5d7 TL |
398 | croak "cannot lookup undefined type!" if !defined($type); |
399 | ||
212b08e8 DM |
400 | my $pdata = $class->private(); |
401 | my $plugin = $pdata->{plugins}->{$type}; | |
402 | ||
403 | die "unknown section type '$type'\n" if !$plugin; | |
404 | ||
405 | return $plugin; | |
406 | } | |
407 | ||
408 | sub lookup_types { | |
409 | my ($class) = @_; | |
410 | ||
411 | my $pdata = $class->private(); | |
cca9c864 | 412 | |
a7f30ebf | 413 | return [ sort keys %{$pdata->{plugins}} ]; |
212b08e8 DM |
414 | } |
415 | ||
416 | sub decode_value { | |
417 | my ($class, $type, $key, $value) = @_; | |
418 | ||
419 | return $value; | |
420 | } | |
421 | ||
422 | sub encode_value { | |
423 | my ($class, $type, $key, $value) = @_; | |
424 | ||
425 | return $value; | |
426 | } | |
427 | ||
428 | sub check_value { | |
429 | my ($class, $type, $key, $value, $storeid, $skipSchemaCheck) = @_; | |
430 | ||
431 | my $pdata = $class->private(); | |
432 | ||
433 | return $value if $key eq 'type' && $type eq $value; | |
434 | ||
435 | my $opts = $pdata->{options}->{$type}; | |
cca9c864 | 436 | die "unknown section type '$type'\n" if !$opts; |
212b08e8 DM |
437 | |
438 | die "unexpected property '$key'\n" if !defined($opts->{$key}); | |
439 | ||
7887b1cb | 440 | my $schema = $class->get_property_schema($type, $key); |
212b08e8 DM |
441 | die "unknown property type\n" if !$schema; |
442 | ||
443 | my $ct = $schema->{type}; | |
444 | ||
445 | $value = 1 if $ct eq 'boolean' && !defined($value); | |
446 | ||
447 | die "got undefined value\n" if !defined($value); | |
448 | ||
449 | die "property contains a line feed\n" if $value =~ m/[\n\r]/; | |
450 | ||
451 | if (!$skipSchemaCheck) { | |
452 | my $errors = {}; | |
69d9edcc DC |
453 | |
454 | my $checkschema = $schema; | |
455 | ||
456 | if ($ct eq 'array') { | |
457 | die "no item schema for array" if !defined($schema->{items}); | |
458 | $checkschema = $schema->{items}; | |
459 | } | |
460 | ||
461 | PVE::JSONSchema::check_prop($value, $checkschema, '', $errors); | |
212b08e8 DM |
462 | if (scalar(keys %$errors)) { |
463 | die "$errors->{$key}\n" if $errors->{$key}; | |
464 | die "$errors->{_root}\n" if $errors->{_root}; | |
465 | die "unknown error\n"; | |
466 | } | |
467 | } | |
468 | ||
bbca3bdc DM |
469 | if ($ct eq 'boolean' || $ct eq 'integer' || $ct eq 'number') { |
470 | return $value + 0; # convert to number | |
471 | } | |
472 | ||
212b08e8 DM |
473 | return $value; |
474 | } | |
475 | ||
476 | sub parse_section_header { | |
477 | my ($class, $line) = @_; | |
478 | ||
479 | if ($line =~ m/^(\S+):\s*(\S+)\s*$/) { | |
480 | my ($type, $sectionId) = ($1, $2); | |
481 | my $errmsg = undef; # set if you want to skip whole section | |
482 | my $config = {}; # to return additional attributes | |
483 | return ($type, $sectionId, $errmsg, $config); | |
484 | } | |
485 | return undef; | |
486 | } | |
487 | ||
488 | sub format_section_header { | |
2d63b598 | 489 | my ($class, $type, $sectionId, $scfg, $done_hash) = @_; |
212b08e8 DM |
490 | |
491 | return "$type: $sectionId\n"; | |
492 | } | |
493 | ||
7887b1cb DC |
494 | sub get_property_schema { |
495 | my ($class, $type, $key) = @_; | |
496 | ||
497 | my $pdata = $class->private(); | |
498 | my $opts = $pdata->{options}->{$type}; | |
499 | ||
500 | my $schema; | |
501 | if ($class->has_isolated_properties()) { | |
502 | $schema = $pdata->{isolatedPropertyList}->{$type}->{$key}; | |
503 | } | |
504 | $schema //= $pdata->{propertyList}->{$key}; | |
505 | ||
506 | return $schema; | |
507 | } | |
212b08e8 DM |
508 | |
509 | sub parse_config { | |
c9ede1c3 | 510 | my ($class, $filename, $raw, $allow_unknown) = @_; |
212b08e8 DM |
511 | |
512 | my $pdata = $class->private(); | |
513 | ||
514 | my $ids = {}; | |
515 | my $order = {}; | |
516 | ||
3c4d612a WB |
517 | $raw = '' if !defined($raw); |
518 | ||
519 | my $digest = Digest::SHA::sha1_hex($raw); | |
cca9c864 | 520 | |
212b08e8 DM |
521 | my $pri = 1; |
522 | ||
523 | my $lineno = 0; | |
3c4d612a WB |
524 | my @lines = split(/\n/, $raw); |
525 | my $nextline = sub { | |
e1fbb779 | 526 | while (defined(my $line = shift @lines)) { |
3c4d612a | 527 | $lineno++; |
e1fbb779 | 528 | return $line if ($line !~ /^\s*#/); |
3c4d612a WB |
529 | } |
530 | }; | |
212b08e8 | 531 | |
69d9edcc DC |
532 | my $is_array = sub { |
533 | my ($type, $key) = @_; | |
534 | ||
7887b1cb | 535 | my $schema = $class->get_property_schema($type, $key); |
69d9edcc DC |
536 | die "unknown property type\n" if !$schema; |
537 | ||
538 | return $schema->{type} eq 'array'; | |
539 | }; | |
540 | ||
4b32ef6e | 541 | my $errors = []; |
e1fbb779 FE |
542 | while (@lines) { |
543 | my $line = $nextline->(); | |
544 | next if !$line; | |
545 | ||
212b08e8 DM |
546 | my $errprefix = "file $filename line $lineno"; |
547 | ||
548 | my ($type, $sectionId, $errmsg, $config) = $class->parse_section_header($line); | |
549 | if ($config) { | |
c9ede1c3 WB |
550 | my $skip = 0; |
551 | my $unknown = 0; | |
212b08e8 DM |
552 | |
553 | my $plugin; | |
554 | ||
555 | if ($errmsg) { | |
c9ede1c3 | 556 | $skip = 1; |
212b08e8 DM |
557 | chomp $errmsg; |
558 | warn "$errprefix (skip section '$sectionId'): $errmsg\n"; | |
559 | } elsif (!$type) { | |
c9ede1c3 | 560 | $skip = 1; |
212b08e8 DM |
561 | warn "$errprefix (skip section '$sectionId'): missing type - internal error\n"; |
562 | } else { | |
563 | if (!($plugin = $pdata->{plugins}->{$type})) { | |
c9ede1c3 WB |
564 | if ($allow_unknown) { |
565 | $unknown = 1; | |
566 | } else { | |
567 | $skip = 1; | |
568 | warn "$errprefix (skip section '$sectionId'): unsupported type '$type'\n"; | |
569 | } | |
212b08e8 DM |
570 | } |
571 | } | |
572 | ||
9e594bd4 | 573 | while ($line = $nextline->()) { |
c9ede1c3 | 574 | next if $skip; # skip |
212b08e8 DM |
575 | |
576 | $errprefix = "file $filename line $lineno"; | |
577 | ||
578 | if ($line =~ m/^\s+(\S+)(\s+(.*\S))?\s*$/) { | |
579 | my ($k, $v) = ($1, $3); | |
cca9c864 | 580 | |
212b08e8 | 581 | eval { |
d2a6411c DC |
582 | if ($unknown) { |
583 | if (!defined($config->{$k})) { | |
584 | $config->{$k} = $v; | |
585 | } else { | |
586 | if (!ref($config->{$k})) { | |
587 | $config->{$k} = [$config->{$k}]; | |
588 | } | |
589 | push $config->{$k}->@*, $v; | |
69d9edcc | 590 | } |
d2a6411c DC |
591 | } elsif ($is_array->($type, $k)) { |
592 | $v = $plugin->check_value($type, $k, $v, $sectionId); | |
69d9edcc DC |
593 | $config->{$k} = [] if !defined($config->{$k}); |
594 | push $config->{$k}->@*, $v; | |
595 | } else { | |
596 | die "duplicate attribute\n" if defined($config->{$k}); | |
d2a6411c | 597 | $v = $plugin->check_value($type, $k, $v, $sectionId); |
69d9edcc | 598 | $config->{$k} = $v; |
c9ede1c3 | 599 | } |
212b08e8 | 600 | }; |
4b32ef6e FE |
601 | if (my $err = $@) { |
602 | warn "$errprefix (section '$sectionId') - unable to parse value of '$k': $err"; | |
603 | push @$errors, { | |
604 | context => $errprefix, | |
605 | section => $sectionId, | |
606 | key => $k, | |
607 | err => $err, | |
608 | }; | |
609 | } | |
212b08e8 DM |
610 | |
611 | } else { | |
612 | warn "$errprefix (section '$sectionId') - ignore config line: $line\n"; | |
613 | } | |
614 | } | |
615 | ||
c9ede1c3 | 616 | if ($unknown) { |
212b08e8 | 617 | $config->{type} = $type; |
c9ede1c3 WB |
618 | $ids->{$sectionId} = $config; |
619 | $order->{$sectionId} = $pri++; | |
620 | } elsif (!$skip && $type && $plugin && $config) { | |
621 | $config->{type} = $type; | |
622 | if (!$unknown) { | |
623 | $config = eval { $config = $plugin->check_config($sectionId, $config, 1, 1); }; | |
624 | warn "$errprefix (skip section '$sectionId'): $@" if $@; | |
625 | } | |
626 | $ids->{$sectionId} = $config; | |
212b08e8 DM |
627 | $order->{$sectionId} = $pri++; |
628 | } | |
629 | ||
630 | } else { | |
631 | warn "$errprefix - ignore config line: $line\n"; | |
632 | } | |
633 | } | |
634 | ||
4b32ef6e FE |
635 | my $cfg = { |
636 | ids => $ids, | |
637 | order => $order, | |
638 | digest => $digest | |
639 | }; | |
640 | $cfg->{errors} = $errors if scalar(@$errors) > 0; | |
212b08e8 DM |
641 | |
642 | return $cfg; | |
643 | } | |
644 | ||
645 | sub check_config { | |
646 | my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_; | |
647 | ||
648 | my $type = $class->type(); | |
649 | my $pdata = $class->private(); | |
650 | my $opts = $pdata->{options}->{$type}; | |
651 | ||
652 | my $settings = { type => $type }; | |
653 | ||
654 | foreach my $k (keys %$config) { | |
655 | my $value = $config->{$k}; | |
d0ba18e1 | 656 | |
212b08e8 | 657 | die "can't change value of fixed parameter '$k'\n" |
95244fd7 | 658 | if !$create && defined($opts->{$k}) && $opts->{$k}->{fixed}; |
d0ba18e1 | 659 | |
212b08e8 DM |
660 | if (defined($value)) { |
661 | my $tmp = $class->check_value($type, $k, $value, $sectionId, $skipSchemaCheck); | |
662 | $settings->{$k} = $class->decode_value($type, $k, $tmp); | |
663 | } else { | |
664 | die "got undefined value for option '$k'\n"; | |
665 | } | |
666 | } | |
667 | ||
668 | if ($create) { | |
669 | # check if we have a value for all required options | |
670 | foreach my $k (keys %$opts) { | |
671 | next if $opts->{$k}->{optional}; | |
672 | die "missing value for required option '$k'\n" | |
673 | if !defined($config->{$k}); | |
674 | } | |
675 | } | |
676 | ||
677 | return $settings; | |
678 | } | |
679 | ||
680 | my $format_config_line = sub { | |
681 | my ($schema, $key, $value) = @_; | |
682 | ||
683 | my $ct = $schema->{type}; | |
684 | ||
0cb4d663 DM |
685 | die "property '$key' contains a line feed\n" |
686 | if ($key =~ m/[\n\r]/) || ($value =~ m/[\n\r]/); | |
687 | ||
212b08e8 | 688 | if ($ct eq 'boolean') { |
d152f8b0 WB |
689 | return "\t$key " . ($value ? 1 : 0) . "\n" |
690 | if defined($value); | |
69d9edcc DC |
691 | } elsif ($ct eq 'array') { |
692 | die "property '$key' is not an array" if ref($value) ne 'ARRAY'; | |
693 | my $result = ''; | |
694 | for my $line ($value->@*) { | |
695 | $result .= "\t$key $line\n" if $value ne ''; | |
696 | } | |
697 | return $result; | |
212b08e8 DM |
698 | } else { |
699 | return "\t$key $value\n" if "$value" ne ''; | |
700 | } | |
701 | }; | |
702 | ||
703 | sub write_config { | |
c9ede1c3 | 704 | my ($class, $filename, $cfg, $allow_unknown) = @_; |
212b08e8 DM |
705 | |
706 | my $pdata = $class->private(); | |
212b08e8 DM |
707 | |
708 | my $out = ''; | |
709 | ||
710 | my $ids = $cfg->{ids}; | |
711 | my $order = $cfg->{order}; | |
712 | ||
713 | my $maxpri = 0; | |
714 | foreach my $sectionId (keys %$ids) { | |
cca9c864 | 715 | my $pri = $order->{$sectionId}; |
212b08e8 DM |
716 | $maxpri = $pri if $pri && $pri > $maxpri; |
717 | } | |
718 | foreach my $sectionId (keys %$ids) { | |
719 | if (!defined ($order->{$sectionId})) { | |
720 | $order->{$sectionId} = ++$maxpri; | |
cca9c864 | 721 | } |
212b08e8 DM |
722 | } |
723 | ||
724 | foreach my $sectionId (sort {$order->{$a} <=> $order->{$b}} keys %$ids) { | |
725 | my $scfg = $ids->{$sectionId}; | |
726 | my $type = $scfg->{type}; | |
727 | my $opts = $pdata->{options}->{$type}; | |
7887b1cb | 728 | my $global_opts = $pdata->{options}->{__global}; |
212b08e8 | 729 | |
c9ede1c3 | 730 | die "unknown section type '$type'\n" if !$opts && !$allow_unknown; |
212b08e8 | 731 | |
2d63b598 DM |
732 | my $done_hash = {}; |
733 | ||
734 | my $data = $class->format_section_header($type, $sectionId, $scfg, $done_hash); | |
c9ede1c3 WB |
735 | |
736 | if (!$opts && $allow_unknown) { | |
737 | $done_hash->{type} = 1; | |
738 | my @first = exists($scfg->{comment}) ? ('comment') : (); | |
739 | for my $k (@first, sort keys %$scfg) { | |
740 | next if defined($done_hash->{$k}); | |
741 | $done_hash->{$k} = 1; | |
742 | my $v = $scfg->{$k}; | |
d2a6411c DC |
743 | my $ref = ref($v); |
744 | if (defined($ref) && $ref eq 'ARRAY') { | |
745 | $data .= "\t$k $_\n" for $v->@*; | |
746 | } else { | |
747 | $data .= "\t$k $v\n"; | |
748 | } | |
c9ede1c3 WB |
749 | } |
750 | $out .= "$data\n"; | |
751 | next; | |
752 | } | |
753 | ||
754 | ||
2d63b598 | 755 | if ($scfg->{comment} && !$done_hash->{comment}) { |
212b08e8 DM |
756 | my $k = 'comment'; |
757 | my $v = $class->encode_value($type, $k, $scfg->{$k}); | |
7887b1cb DC |
758 | my $prop = $class->get_property_schema($type, $k); |
759 | $data .= &$format_config_line($prop, $k, $v); | |
212b08e8 DM |
760 | } |
761 | ||
2d63b598 | 762 | $data .= "\tdisable\n" if $scfg->{disable} && !$done_hash->{disable}; |
212b08e8 | 763 | |
2d63b598 DM |
764 | $done_hash->{comment} = 1; |
765 | $done_hash->{disable} = 1; | |
212b08e8 | 766 | |
df89e5eb DM |
767 | my @option_keys = sort keys %$opts; |
768 | foreach my $k (@option_keys) { | |
2d63b598 | 769 | next if defined($done_hash->{$k}); |
212b08e8 DM |
770 | next if $opts->{$k}->{optional}; |
771 | $done_hash->{$k} = 1; | |
772 | my $v = $scfg->{$k}; | |
773 | die "section '$sectionId' - missing value for required option '$k'\n" | |
774 | if !defined ($v); | |
775 | $v = $class->encode_value($type, $k, $v); | |
7887b1cb DC |
776 | my $prop = $class->get_property_schema($type, $k); |
777 | $data .= &$format_config_line($prop, $k, $v); | |
212b08e8 DM |
778 | } |
779 | ||
df89e5eb | 780 | foreach my $k (@option_keys) { |
212b08e8 DM |
781 | next if defined($done_hash->{$k}); |
782 | my $v = $scfg->{$k}; | |
783 | next if !defined($v); | |
784 | $v = $class->encode_value($type, $k, $v); | |
7887b1cb DC |
785 | my $prop = $class->get_property_schema($type, $k); |
786 | $data .= &$format_config_line($prop, $k, $v); | |
212b08e8 DM |
787 | } |
788 | ||
789 | $out .= "$data\n"; | |
790 | } | |
791 | ||
792 | return $out; | |
793 | } | |
794 | ||
795 | sub assert_if_modified { | |
796 | my ($cfg, $digest) = @_; | |
797 | ||
a345b9d5 | 798 | PVE::Tools::assert_if_modified($cfg->{digest}, $digest); |
212b08e8 DM |
799 | } |
800 | ||
5028848d DC |
801 | sub delete_from_config { |
802 | my ($config, $option_schema, $new_options, $to_delete) = @_; | |
803 | ||
804 | for my $k ($to_delete->@*) { | |
805 | my $d = $option_schema->{$k} || die "no such option '$k'\n"; | |
806 | die "unable to delete required option '$k'\n" if !$d->{optional}; | |
807 | die "unable to delete fixed option '$k'\n" if $d->{fixed}; | |
808 | die "cannot set and delete property '$k' at the same time!\n" | |
809 | if defined($new_options->{$k}); | |
810 | delete $config->{$k}; | |
811 | } | |
812 | ||
813 | return $config; | |
814 | } | |
815 | ||
212b08e8 | 816 | 1; |