]> git.proxmox.com Git - pve-common.git/blob - src/PVE/SectionConfig.pm
section config: allow full property-isolation for plugins
[pve-common.git] / src / PVE / SectionConfig.pm
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(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 sub init {
330 my ($class, $property_isolation) = @_;
331
332 my $pdata = $class->private();
333
334 foreach my $k (qw(options plugins plugindata propertyList isolatedPropertyList)) {
335 $pdata->{$k} = {} if !$pdata->{$k};
336 }
337
338 my $plugins = $pdata->{plugins};
339 my $propertyList = $pdata->{propertyList};
340 my $isolatedPropertyList = $pdata->{isolatedPropertyList};
341
342 foreach my $type (keys %$plugins) {
343 my $props = $plugins->{$type}->properties();
344 foreach my $p (keys %$props) {
345 my $res;
346 if ($property_isolation) {
347 $res = $isolatedPropertyList->{$type}->{$p} = {};
348 } else {
349 die "duplicate property '$p'" if defined($propertyList->{$p});
350 $res = $propertyList->{$p} = {};
351 }
352 my $data = $props->{$p};
353 for my $a (keys %$data) {
354 $res->{$a} = $data->{$a};
355 }
356 $res->{optional} = 1;
357 }
358 }
359
360 foreach my $type (keys %$plugins) {
361 my $opts = $plugins->{$type}->options();
362 foreach my $p (keys %$opts) {
363 my $prop;
364 if ($property_isolation) {
365 $prop = $isolatedPropertyList->{$type}->{$p};
366 }
367 $prop //= $propertyList->{$p};
368 die "undefined property '$p'" if !$prop;
369 }
370
371 # automatically the properties to options (if not specified explicitly)
372 if ($property_isolation) {
373 foreach my $p (keys $isolatedPropertyList->{$type}->%*) {
374 next if $opts->{$p};
375 $opts->{$p} = {};
376 $opts->{$p}->{optional} = 1 if $isolatedPropertyList->{$type}->{$p}->{optional};
377 }
378 }
379
380 $pdata->{options}->{$type} = $opts;
381 }
382
383 $propertyList->{type}->{type} = 'string';
384 $propertyList->{type}->{enum} = [sort keys %$plugins];
385 }
386
387 sub lookup {
388 my ($class, $type) = @_;
389
390 croak "cannot lookup undefined type!" if !defined($type);
391
392 my $pdata = $class->private();
393 my $plugin = $pdata->{plugins}->{$type};
394
395 die "unknown section type '$type'\n" if !$plugin;
396
397 return $plugin;
398 }
399
400 sub lookup_types {
401 my ($class) = @_;
402
403 my $pdata = $class->private();
404
405 return [ sort keys %{$pdata->{plugins}} ];
406 }
407
408 sub decode_value {
409 my ($class, $type, $key, $value) = @_;
410
411 return $value;
412 }
413
414 sub encode_value {
415 my ($class, $type, $key, $value) = @_;
416
417 return $value;
418 }
419
420 sub check_value {
421 my ($class, $type, $key, $value, $storeid, $skipSchemaCheck) = @_;
422
423 my $pdata = $class->private();
424
425 return $value if $key eq 'type' && $type eq $value;
426
427 my $opts = $pdata->{options}->{$type};
428 die "unknown section type '$type'\n" if !$opts;
429
430 die "unexpected property '$key'\n" if !defined($opts->{$key});
431
432 my $schema = $class->get_property_schema($type, $key);
433 die "unknown property type\n" if !$schema;
434
435 my $ct = $schema->{type};
436
437 $value = 1 if $ct eq 'boolean' && !defined($value);
438
439 die "got undefined value\n" if !defined($value);
440
441 die "property contains a line feed\n" if $value =~ m/[\n\r]/;
442
443 if (!$skipSchemaCheck) {
444 my $errors = {};
445
446 my $checkschema = $schema;
447
448 if ($ct eq 'array') {
449 die "no item schema for array" if !defined($schema->{items});
450 $checkschema = $schema->{items};
451 }
452
453 PVE::JSONSchema::check_prop($value, $checkschema, '', $errors);
454 if (scalar(keys %$errors)) {
455 die "$errors->{$key}\n" if $errors->{$key};
456 die "$errors->{_root}\n" if $errors->{_root};
457 die "unknown error\n";
458 }
459 }
460
461 if ($ct eq 'boolean' || $ct eq 'integer' || $ct eq 'number') {
462 return $value + 0; # convert to number
463 }
464
465 return $value;
466 }
467
468 sub parse_section_header {
469 my ($class, $line) = @_;
470
471 if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
472 my ($type, $sectionId) = ($1, $2);
473 my $errmsg = undef; # set if you want to skip whole section
474 my $config = {}; # to return additional attributes
475 return ($type, $sectionId, $errmsg, $config);
476 }
477 return undef;
478 }
479
480 sub format_section_header {
481 my ($class, $type, $sectionId, $scfg, $done_hash) = @_;
482
483 return "$type: $sectionId\n";
484 }
485
486 sub get_property_schema {
487 my ($class, $type, $key) = @_;
488
489 my $pdata = $class->private();
490 my $opts = $pdata->{options}->{$type};
491
492 my $schema;
493 if ($class->has_isolated_properties()) {
494 $schema = $pdata->{isolatedPropertyList}->{$type}->{$key};
495 }
496 $schema //= $pdata->{propertyList}->{$key};
497
498 return $schema;
499 }
500
501 sub parse_config {
502 my ($class, $filename, $raw, $allow_unknown) = @_;
503
504 my $pdata = $class->private();
505
506 my $ids = {};
507 my $order = {};
508
509 $raw = '' if !defined($raw);
510
511 my $digest = Digest::SHA::sha1_hex($raw);
512
513 my $pri = 1;
514
515 my $lineno = 0;
516 my @lines = split(/\n/, $raw);
517 my $nextline = sub {
518 while (defined(my $line = shift @lines)) {
519 $lineno++;
520 return $line if ($line !~ /^\s*#/);
521 }
522 };
523
524 my $is_array = sub {
525 my ($type, $key) = @_;
526
527 my $schema = $class->get_property_schema($type, $key);
528 die "unknown property type\n" if !$schema;
529
530 return $schema->{type} eq 'array';
531 };
532
533 my $errors = [];
534 while (@lines) {
535 my $line = $nextline->();
536 next if !$line;
537
538 my $errprefix = "file $filename line $lineno";
539
540 my ($type, $sectionId, $errmsg, $config) = $class->parse_section_header($line);
541 if ($config) {
542 my $skip = 0;
543 my $unknown = 0;
544
545 my $plugin;
546
547 if ($errmsg) {
548 $skip = 1;
549 chomp $errmsg;
550 warn "$errprefix (skip section '$sectionId'): $errmsg\n";
551 } elsif (!$type) {
552 $skip = 1;
553 warn "$errprefix (skip section '$sectionId'): missing type - internal error\n";
554 } else {
555 if (!($plugin = $pdata->{plugins}->{$type})) {
556 if ($allow_unknown) {
557 $unknown = 1;
558 } else {
559 $skip = 1;
560 warn "$errprefix (skip section '$sectionId'): unsupported type '$type'\n";
561 }
562 }
563 }
564
565 while ($line = $nextline->()) {
566 next if $skip; # skip
567
568 $errprefix = "file $filename line $lineno";
569
570 if ($line =~ m/^\s+(\S+)(\s+(.*\S))?\s*$/) {
571 my ($k, $v) = ($1, $3);
572
573 eval {
574 if ($unknown) {
575 if (!defined($config->{$k})) {
576 $config->{$k} = $v;
577 } else {
578 if (!ref($config->{$k})) {
579 $config->{$k} = [$config->{$k}];
580 }
581 push $config->{$k}->@*, $v;
582 }
583 } elsif ($is_array->($type, $k)) {
584 $v = $plugin->check_value($type, $k, $v, $sectionId);
585 $config->{$k} = [] if !defined($config->{$k});
586 push $config->{$k}->@*, $v;
587 } else {
588 die "duplicate attribute\n" if defined($config->{$k});
589 $v = $plugin->check_value($type, $k, $v, $sectionId);
590 $config->{$k} = $v;
591 }
592 };
593 if (my $err = $@) {
594 warn "$errprefix (section '$sectionId') - unable to parse value of '$k': $err";
595 push @$errors, {
596 context => $errprefix,
597 section => $sectionId,
598 key => $k,
599 err => $err,
600 };
601 }
602
603 } else {
604 warn "$errprefix (section '$sectionId') - ignore config line: $line\n";
605 }
606 }
607
608 if ($unknown) {
609 $config->{type} = $type;
610 $ids->{$sectionId} = $config;
611 $order->{$sectionId} = $pri++;
612 } elsif (!$skip && $type && $plugin && $config) {
613 $config->{type} = $type;
614 if (!$unknown) {
615 $config = eval { $config = $plugin->check_config($sectionId, $config, 1, 1); };
616 warn "$errprefix (skip section '$sectionId'): $@" if $@;
617 }
618 $ids->{$sectionId} = $config;
619 $order->{$sectionId} = $pri++;
620 }
621
622 } else {
623 warn "$errprefix - ignore config line: $line\n";
624 }
625 }
626
627 my $cfg = {
628 ids => $ids,
629 order => $order,
630 digest => $digest
631 };
632 $cfg->{errors} = $errors if scalar(@$errors) > 0;
633
634 return $cfg;
635 }
636
637 sub check_config {
638 my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
639
640 my $type = $class->type();
641 my $pdata = $class->private();
642 my $opts = $pdata->{options}->{$type};
643
644 my $settings = { type => $type };
645
646 foreach my $k (keys %$config) {
647 my $value = $config->{$k};
648
649 die "can't change value of fixed parameter '$k'\n"
650 if !$create && defined($opts->{$k}) && $opts->{$k}->{fixed};
651
652 if (defined($value)) {
653 my $tmp = $class->check_value($type, $k, $value, $sectionId, $skipSchemaCheck);
654 $settings->{$k} = $class->decode_value($type, $k, $tmp);
655 } else {
656 die "got undefined value for option '$k'\n";
657 }
658 }
659
660 if ($create) {
661 # check if we have a value for all required options
662 foreach my $k (keys %$opts) {
663 next if $opts->{$k}->{optional};
664 die "missing value for required option '$k'\n"
665 if !defined($config->{$k});
666 }
667 }
668
669 return $settings;
670 }
671
672 my $format_config_line = sub {
673 my ($schema, $key, $value) = @_;
674
675 my $ct = $schema->{type};
676
677 die "property '$key' contains a line feed\n"
678 if ($key =~ m/[\n\r]/) || ($value =~ m/[\n\r]/);
679
680 if ($ct eq 'boolean') {
681 return "\t$key " . ($value ? 1 : 0) . "\n"
682 if defined($value);
683 } elsif ($ct eq 'array') {
684 die "property '$key' is not an array" if ref($value) ne 'ARRAY';
685 my $result = '';
686 for my $line ($value->@*) {
687 $result .= "\t$key $line\n" if $value ne '';
688 }
689 return $result;
690 } else {
691 return "\t$key $value\n" if "$value" ne '';
692 }
693 };
694
695 sub write_config {
696 my ($class, $filename, $cfg, $allow_unknown) = @_;
697
698 my $pdata = $class->private();
699
700 my $out = '';
701
702 my $ids = $cfg->{ids};
703 my $order = $cfg->{order};
704
705 my $maxpri = 0;
706 foreach my $sectionId (keys %$ids) {
707 my $pri = $order->{$sectionId};
708 $maxpri = $pri if $pri && $pri > $maxpri;
709 }
710 foreach my $sectionId (keys %$ids) {
711 if (!defined ($order->{$sectionId})) {
712 $order->{$sectionId} = ++$maxpri;
713 }
714 }
715
716 foreach my $sectionId (sort {$order->{$a} <=> $order->{$b}} keys %$ids) {
717 my $scfg = $ids->{$sectionId};
718 my $type = $scfg->{type};
719 my $opts = $pdata->{options}->{$type};
720 my $global_opts = $pdata->{options}->{__global};
721
722 die "unknown section type '$type'\n" if !$opts && !$allow_unknown;
723
724 my $done_hash = {};
725
726 my $data = $class->format_section_header($type, $sectionId, $scfg, $done_hash);
727
728 if (!$opts && $allow_unknown) {
729 $done_hash->{type} = 1;
730 my @first = exists($scfg->{comment}) ? ('comment') : ();
731 for my $k (@first, sort keys %$scfg) {
732 next if defined($done_hash->{$k});
733 $done_hash->{$k} = 1;
734 my $v = $scfg->{$k};
735 my $ref = ref($v);
736 if (defined($ref) && $ref eq 'ARRAY') {
737 $data .= "\t$k $_\n" for $v->@*;
738 } else {
739 $data .= "\t$k $v\n";
740 }
741 }
742 $out .= "$data\n";
743 next;
744 }
745
746
747 if ($scfg->{comment} && !$done_hash->{comment}) {
748 my $k = 'comment';
749 my $v = $class->encode_value($type, $k, $scfg->{$k});
750 my $prop = $class->get_property_schema($type, $k);
751 $data .= &$format_config_line($prop, $k, $v);
752 }
753
754 $data .= "\tdisable\n" if $scfg->{disable} && !$done_hash->{disable};
755
756 $done_hash->{comment} = 1;
757 $done_hash->{disable} = 1;
758
759 my @option_keys = sort keys %$opts;
760 foreach my $k (@option_keys) {
761 next if defined($done_hash->{$k});
762 next if $opts->{$k}->{optional};
763 $done_hash->{$k} = 1;
764 my $v = $scfg->{$k};
765 die "section '$sectionId' - missing value for required option '$k'\n"
766 if !defined ($v);
767 $v = $class->encode_value($type, $k, $v);
768 my $prop = $class->get_property_schema($type, $k);
769 $data .= &$format_config_line($prop, $k, $v);
770 }
771
772 foreach my $k (@option_keys) {
773 next if defined($done_hash->{$k});
774 my $v = $scfg->{$k};
775 next if !defined($v);
776 $v = $class->encode_value($type, $k, $v);
777 my $prop = $class->get_property_schema($type, $k);
778 $data .= &$format_config_line($prop, $k, $v);
779 }
780
781 $out .= "$data\n";
782 }
783
784 return $out;
785 }
786
787 sub assert_if_modified {
788 my ($cfg, $digest) = @_;
789
790 PVE::Tools::assert_if_modified($cfg->{digest}, $digest);
791 }
792
793 sub delete_from_config {
794 my ($config, $option_schema, $new_options, $to_delete) = @_;
795
796 for my $k ($to_delete->@*) {
797 my $d = $option_schema->{$k} || die "no such option '$k'\n";
798 die "unable to delete required option '$k'\n" if !$d->{optional};
799 die "unable to delete fixed option '$k'\n" if $d->{fixed};
800 die "cannot set and delete property '$k' at the same time!\n"
801 if defined($new_options->{$k});
802 delete $config->{$k};
803 }
804
805 return $config;
806 }
807
808 1;