]> git.proxmox.com Git - pve-common.git/blame - src/PVE/SectionConfig.pm
bump version to 8.2.1
[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);
7887b1cb
DC
11use 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
73my $defaultData = {
74 options => {},
75 plugins => {},
76 plugindata => {},
77 propertyList => {},
78};
79
80sub private {
81 die "overwrite me";
82 return $defaultData;
83}
84
85sub 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
99sub type {
100 die "overwrite me";
101}
102
103sub properties {
104 return {};
105}
106
107sub options {
108 return {};
cca9c864 109}
212b08e8
DM
110
111sub plugindata {
112 return {};
cca9c864 113}
212b08e8 114
7887b1cb
DC
115sub has_isolated_properties {
116 my ($class) = @_;
117
118 my $isolatedPropertyList = $class->private()->{isolatedPropertyList};
119
120 return defined($isolatedPropertyList) && scalar(keys $isolatedPropertyList->%*) > 0;
121}
122
123my 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
136my 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 171sub 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
245sub 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 335sub 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
395sub 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
408sub lookup_types {
409 my ($class) = @_;
410
411 my $pdata = $class->private();
cca9c864 412
a7f30ebf 413 return [ sort keys %{$pdata->{plugins}} ];
212b08e8
DM
414}
415
416sub decode_value {
417 my ($class, $type, $key, $value) = @_;
418
419 return $value;
420}
421
422sub encode_value {
423 my ($class, $type, $key, $value) = @_;
424
425 return $value;
426}
427
428sub 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
476sub 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
488sub format_section_header {
2d63b598 489 my ($class, $type, $sectionId, $scfg, $done_hash) = @_;
212b08e8
DM
490
491 return "$type: $sectionId\n";
492}
493
7887b1cb
DC
494sub 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
509sub 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
645sub 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
680my $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
703sub 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
795sub assert_if_modified {
796 my ($cfg, $digest) = @_;
797
a345b9d5 798 PVE::Tools::assert_if_modified($cfg->{digest}, $digest);
212b08e8
DM
799}
800
5028848d
DC
801sub 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 8161;