]>
Commit | Line | Data |
---|---|---|
1 | package PVE::SectionConfig; | |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use Carp; | |
7 | use Digest::SHA; | |
8 | ||
9 | use PVE::Exception qw(raise_param_exc); | |
10 | use PVE::JSONSchema qw(get_standard_option); | |
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 | # ``` | |
64 | # PVE::Dummy::BasePlugin->init(property_isolation => 1); | |
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. | |
72 | ||
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 | ||
91 | die "duplicate plugin registration (type = $type)" | |
92 | if defined($pdata->{plugins}->{$type}); | |
93 | ||
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 {}; | |
109 | } | |
110 | ||
111 | sub plugindata { | |
112 | return {}; | |
113 | } | |
114 | ||
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 | ||
171 | sub createSchema { | |
172 | my ($class, $skip_type, $base) = @_; | |
173 | ||
174 | my $pdata = $class->private(); | |
175 | my $propertyList = $pdata->{propertyList}; | |
176 | my $plugins = $pdata->{plugins}; | |
177 | ||
178 | my $props = $base || {}; | |
179 | ||
180 | if (!$class->has_isolated_properties()) { | |
181 | foreach my $p (keys %$propertyList) { | |
182 | next if $skip_type && $p eq 'type'; | |
183 | ||
184 | if (!$propertyList->{$p}->{optional}) { | |
185 | $props->{$p} = $propertyList->{$p}; | |
186 | next; | |
187 | } | |
188 | ||
189 | my $required = 1; | |
190 | ||
191 | my $copts = $class->options(); | |
192 | $required = 0 if defined($copts->{$p}) && $copts->{$p}->{optional}; | |
193 | ||
194 | foreach my $t (keys %$plugins) { | |
195 | my $opts = $pdata->{options}->{$t} || {}; | |
196 | $required = 0 if !defined($opts->{$p}) || $opts->{$p}->{optional}; | |
197 | } | |
198 | ||
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 | } | |
207 | } | |
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 | } | |
235 | } | |
236 | } | |
237 | ||
238 | return { | |
239 | type => "object", | |
240 | additionalProperties => 0, | |
241 | properties => $props, | |
242 | }; | |
243 | } | |
244 | ||
245 | sub updateSchema { | |
246 | my ($class, $single_class, $base) = @_; | |
247 | ||
248 | my $pdata = $class->private(); | |
249 | my $propertyList = $pdata->{propertyList}; | |
250 | my $plugins = $pdata->{plugins}; | |
251 | ||
252 | my $props = $base || {}; | |
253 | ||
254 | my $filter_type = $single_class ? $class->type() : undef; | |
255 | ||
256 | if (!$class->has_isolated_properties()) { | |
257 | foreach my $p (keys %$propertyList) { | |
258 | next if $p eq 'type'; | |
259 | ||
260 | my $copts = $class->options(); | |
261 | ||
262 | next if defined($filter_type) && !defined($copts->{$p}); | |
263 | ||
264 | if (!$propertyList->{$p}->{optional}) { | |
265 | $props->{$p} = $propertyList->{$p}; | |
266 | next; | |
267 | } | |
268 | ||
269 | my $modifyable = 0; | |
270 | ||
271 | $modifyable = 1 if defined($copts->{$p}) && !$copts->{$p}->{fixed}; | |
272 | ||
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}->%*}; | |
301 | } | |
302 | ||
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 | } | |
311 | } | |
312 | ||
313 | $props->{digest} = get_standard_option('pve-config-digest'); | |
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 | ||
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. | |
335 | sub init { | |
336 | my ($class, %param) = @_; | |
337 | ||
338 | my $property_isolation = $param{property_isolation}; | |
339 | ||
340 | my $pdata = $class->private(); | |
341 | ||
342 | foreach my $k (qw(options plugins plugindata propertyList isolatedPropertyList)) { | |
343 | $pdata->{$k} = {} if !$pdata->{$k}; | |
344 | } | |
345 | ||
346 | my $plugins = $pdata->{plugins}; | |
347 | my $propertyList = $pdata->{propertyList}; | |
348 | my $isolatedPropertyList = $pdata->{isolatedPropertyList}; | |
349 | ||
350 | foreach my $type (keys %$plugins) { | |
351 | my $props = $plugins->{$type}->properties(); | |
352 | foreach my $p (keys %$props) { | |
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 | } | |
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) { | |
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 | } | |
386 | } | |
387 | ||
388 | $pdata->{options}->{$type} = $opts; | |
389 | } | |
390 | ||
391 | $propertyList->{type}->{type} = 'string'; | |
392 | $propertyList->{type}->{enum} = [sort keys %$plugins]; | |
393 | } | |
394 | ||
395 | sub lookup { | |
396 | my ($class, $type) = @_; | |
397 | ||
398 | croak "cannot lookup undefined type!" if !defined($type); | |
399 | ||
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(); | |
412 | ||
413 | return [ sort keys %{$pdata->{plugins}} ]; | |
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}; | |
436 | die "unknown section type '$type'\n" if !$opts; | |
437 | ||
438 | die "unexpected property '$key'\n" if !defined($opts->{$key}); | |
439 | ||
440 | my $schema = $class->get_property_schema($type, $key); | |
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 = {}; | |
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); | |
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 | ||
469 | if ($ct eq 'boolean' || $ct eq 'integer' || $ct eq 'number') { | |
470 | return $value + 0; # convert to number | |
471 | } | |
472 | ||
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 { | |
489 | my ($class, $type, $sectionId, $scfg, $done_hash) = @_; | |
490 | ||
491 | return "$type: $sectionId\n"; | |
492 | } | |
493 | ||
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 | } | |
508 | ||
509 | sub parse_config { | |
510 | my ($class, $filename, $raw, $allow_unknown) = @_; | |
511 | ||
512 | my $pdata = $class->private(); | |
513 | ||
514 | my $ids = {}; | |
515 | my $order = {}; | |
516 | ||
517 | $raw = '' if !defined($raw); | |
518 | ||
519 | my $digest = Digest::SHA::sha1_hex($raw); | |
520 | ||
521 | my $pri = 1; | |
522 | ||
523 | my $lineno = 0; | |
524 | my @lines = split(/\n/, $raw); | |
525 | my $nextline = sub { | |
526 | while (defined(my $line = shift @lines)) { | |
527 | $lineno++; | |
528 | return $line if ($line !~ /^\s*#/); | |
529 | } | |
530 | }; | |
531 | ||
532 | my $is_array = sub { | |
533 | my ($type, $key) = @_; | |
534 | ||
535 | my $schema = $class->get_property_schema($type, $key); | |
536 | die "unknown property type\n" if !$schema; | |
537 | ||
538 | return $schema->{type} eq 'array'; | |
539 | }; | |
540 | ||
541 | my $errors = []; | |
542 | while (@lines) { | |
543 | my $line = $nextline->(); | |
544 | next if !$line; | |
545 | ||
546 | my $errprefix = "file $filename line $lineno"; | |
547 | ||
548 | my ($type, $sectionId, $errmsg, $config) = $class->parse_section_header($line); | |
549 | if ($config) { | |
550 | my $skip = 0; | |
551 | my $unknown = 0; | |
552 | ||
553 | my $plugin; | |
554 | ||
555 | if ($errmsg) { | |
556 | $skip = 1; | |
557 | chomp $errmsg; | |
558 | warn "$errprefix (skip section '$sectionId'): $errmsg\n"; | |
559 | } elsif (!$type) { | |
560 | $skip = 1; | |
561 | warn "$errprefix (skip section '$sectionId'): missing type - internal error\n"; | |
562 | } else { | |
563 | if (!($plugin = $pdata->{plugins}->{$type})) { | |
564 | if ($allow_unknown) { | |
565 | $unknown = 1; | |
566 | } else { | |
567 | $skip = 1; | |
568 | warn "$errprefix (skip section '$sectionId'): unsupported type '$type'\n"; | |
569 | } | |
570 | } | |
571 | } | |
572 | ||
573 | while ($line = $nextline->()) { | |
574 | next if $skip; # skip | |
575 | ||
576 | $errprefix = "file $filename line $lineno"; | |
577 | ||
578 | if ($line =~ m/^\s+(\S+)(\s+(.*\S))?\s*$/) { | |
579 | my ($k, $v) = ($1, $3); | |
580 | ||
581 | eval { | |
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; | |
590 | } | |
591 | } elsif ($is_array->($type, $k)) { | |
592 | $v = $plugin->check_value($type, $k, $v, $sectionId); | |
593 | $config->{$k} = [] if !defined($config->{$k}); | |
594 | push $config->{$k}->@*, $v; | |
595 | } else { | |
596 | die "duplicate attribute\n" if defined($config->{$k}); | |
597 | $v = $plugin->check_value($type, $k, $v, $sectionId); | |
598 | $config->{$k} = $v; | |
599 | } | |
600 | }; | |
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 | } | |
610 | ||
611 | } else { | |
612 | warn "$errprefix (section '$sectionId') - ignore config line: $line\n"; | |
613 | } | |
614 | } | |
615 | ||
616 | if ($unknown) { | |
617 | $config->{type} = $type; | |
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; | |
627 | $order->{$sectionId} = $pri++; | |
628 | } | |
629 | ||
630 | } else { | |
631 | warn "$errprefix - ignore config line: $line\n"; | |
632 | } | |
633 | } | |
634 | ||
635 | my $cfg = { | |
636 | ids => $ids, | |
637 | order => $order, | |
638 | digest => $digest | |
639 | }; | |
640 | $cfg->{errors} = $errors if scalar(@$errors) > 0; | |
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}; | |
656 | ||
657 | die "can't change value of fixed parameter '$k'\n" | |
658 | if !$create && defined($opts->{$k}) && $opts->{$k}->{fixed}; | |
659 | ||
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 | ||
685 | die "property '$key' contains a line feed\n" | |
686 | if ($key =~ m/[\n\r]/) || ($value =~ m/[\n\r]/); | |
687 | ||
688 | if ($ct eq 'boolean') { | |
689 | return "\t$key " . ($value ? 1 : 0) . "\n" | |
690 | if defined($value); | |
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; | |
698 | } else { | |
699 | return "\t$key $value\n" if "$value" ne ''; | |
700 | } | |
701 | }; | |
702 | ||
703 | sub write_config { | |
704 | my ($class, $filename, $cfg, $allow_unknown) = @_; | |
705 | ||
706 | my $pdata = $class->private(); | |
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) { | |
715 | my $pri = $order->{$sectionId}; | |
716 | $maxpri = $pri if $pri && $pri > $maxpri; | |
717 | } | |
718 | foreach my $sectionId (keys %$ids) { | |
719 | if (!defined ($order->{$sectionId})) { | |
720 | $order->{$sectionId} = ++$maxpri; | |
721 | } | |
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}; | |
728 | my $global_opts = $pdata->{options}->{__global}; | |
729 | ||
730 | die "unknown section type '$type'\n" if !$opts && !$allow_unknown; | |
731 | ||
732 | my $done_hash = {}; | |
733 | ||
734 | my $data = $class->format_section_header($type, $sectionId, $scfg, $done_hash); | |
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}; | |
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 | } | |
749 | } | |
750 | $out .= "$data\n"; | |
751 | next; | |
752 | } | |
753 | ||
754 | ||
755 | if ($scfg->{comment} && !$done_hash->{comment}) { | |
756 | my $k = 'comment'; | |
757 | my $v = $class->encode_value($type, $k, $scfg->{$k}); | |
758 | my $prop = $class->get_property_schema($type, $k); | |
759 | $data .= &$format_config_line($prop, $k, $v); | |
760 | } | |
761 | ||
762 | $data .= "\tdisable\n" if $scfg->{disable} && !$done_hash->{disable}; | |
763 | ||
764 | $done_hash->{comment} = 1; | |
765 | $done_hash->{disable} = 1; | |
766 | ||
767 | my @option_keys = sort keys %$opts; | |
768 | foreach my $k (@option_keys) { | |
769 | next if defined($done_hash->{$k}); | |
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); | |
776 | my $prop = $class->get_property_schema($type, $k); | |
777 | $data .= &$format_config_line($prop, $k, $v); | |
778 | } | |
779 | ||
780 | foreach my $k (@option_keys) { | |
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); | |
785 | my $prop = $class->get_property_schema($type, $k); | |
786 | $data .= &$format_config_line($prop, $k, $v); | |
787 | } | |
788 | ||
789 | $out .= "$data\n"; | |
790 | } | |
791 | ||
792 | return $out; | |
793 | } | |
794 | ||
795 | sub assert_if_modified { | |
796 | my ($cfg, $digest) = @_; | |
797 | ||
798 | PVE::Tools::assert_if_modified($cfg->{digest}, $digest); | |
799 | } | |
800 | ||
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 | ||
816 | 1; |