]>
git.proxmox.com Git - pve-common.git/blob - src/PVE/SectionConfig.pm
1 package PVE
::SectionConfig
;
9 use PVE
::Exception
qw(raise_param_exc);
10 use PVE
::JSONSchema
qw(get_standard_option);
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".
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'
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:
31 # 'some-optional-property' => { optional => 1 },
32 # 'a-fixed-property' => { fixed => 1 },
33 # 'a-required-but-not-fixed-property' => {},
38 # 'fixed' options can be set on create, but not changed afterwards.
40 # To actually use it, you have to first register all the plugins and then init
41 # the 'base' plugin, like so:
44 # use PVE::Dummy::Plugin1;
45 # use PVE::Dummy::Plugin2;
46 # use PVE::Dummy::BasePlugin;
48 # PVE::Dummy::Plugin1->register();
49 # PVE::Dummy::Plugin2->register();
50 # PVE::Dummy::BasePlugin->init();
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.
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:
64 # PVE::Dummy::BasePlugin->init(property_isolation => 1);
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
88 my $type = $class->type();
89 my $pdata = $class->private();
91 die "duplicate plugin registration (type = $type)"
92 if defined($pdata->{plugins
}->{$type});
94 my $plugindata = $class->plugindata();
95 $pdata->{plugindata
}->{$type} = $plugindata;
96 $pdata->{plugins
}->{$type} = $class;
115 sub has_isolated_properties
{
118 my $isolatedPropertyList = $class->private()->{isolatedPropertyList
};
120 return defined($isolatedPropertyList) && scalar(keys $isolatedPropertyList->%*) > 0;
123 my sub compare_property
{
124 my ($a, $b, $skip_opts) = @_;
126 my $merged = {$a->%*, $b->%*};
127 delete $merged->{$_} for $skip_opts->@*;
129 for my $opt (keys $merged->%*) {
130 return 0 if !PVE
::Tools
::is_deeply
($a->{$opt}, $b->{$opt});
136 my sub add_property
{
137 my ($props, $key, $prop, $type) = @_;
139 if (!defined($props->{$key})) {
140 $props->{$key} = $prop;
144 if (!defined($props->{$key}->{oneOf
})) {
145 if (compare_property
($props->{$key}, $prop, ['instance-types'])) {
146 push $props->{$key}->{'instance-types'}->@*, $type;
148 my $new_prop = delete $props->{$key};
149 delete $new_prop->{'type-property'};
150 delete $prop->{'type-property'};
152 'type-property' => 'type',
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;
167 push $props->{$key}->{oneOf
}->@*, $prop;
172 my ($class, $skip_type, $base) = @_;
174 my $pdata = $class->private();
175 my $propertyList = $pdata->{propertyList
};
176 my $plugins = $pdata->{plugins
};
178 my $props = $base || {};
180 if (!$class->has_isolated_properties()) {
181 foreach my $p (keys %$propertyList) {
182 next if $skip_type && $p eq 'type';
184 if (!$propertyList->{$p}->{optional
}) {
185 $props->{$p} = $propertyList->{$p};
191 my $copts = $class->options();
192 $required = 0 if defined($copts->{$p}) && $copts->{$p}->{optional
};
194 foreach my $t (keys %$plugins) {
195 my $opts = $pdata->{options
}->{$t} || {};
196 $required = 0 if !defined($opts->{$p}) || $opts->{$p}->{optional
};
200 # make a copy, because we modify the optional property
201 my $res = {$propertyList->{$p}->%*}; # shallow copy
202 $res->{optional
} = 0;
205 $props->{$p} = $propertyList->{$p};
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
};
218 add_property
($props, $key, $prop, $type);
221 # add remaining global properties
222 for my $opt (keys $propertyList->%*) {
223 next if $props->{$opt};
224 $props->{$opt} = {$propertyList->{$opt}->%*};
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'};
232 $props->{$opt}->{optional
} = 1;
240 additionalProperties
=> 0,
241 properties
=> $props,
246 my ($class, $single_class, $base) = @_;
248 my $pdata = $class->private();
249 my $propertyList = $pdata->{propertyList
};
250 my $plugins = $pdata->{plugins
};
252 my $props = $base || {};
254 my $filter_type = $single_class ?
$class->type() : undef;
256 if (!$class->has_isolated_properties()) {
257 foreach my $p (keys %$propertyList) {
258 next if $p eq 'type';
260 my $copts = $class->options();
262 next if defined($filter_type) && !defined($copts->{$p});
264 if (!$propertyList->{$p}->{optional
}) {
265 $props->{$p} = $propertyList->{$p};
271 $modifyable = 1 if defined($copts->{$p}) && !$copts->{$p}->{fixed
};
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
};
278 next if !$modifyable;
280 $props->{$p} = $propertyList->{$p};
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
};
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;
294 add_property
($props, $key, $prop, $type);
298 for my $opt (keys $propertyList->%*) {
299 next if $props->{$opt};
300 $props->{$opt} = {$propertyList->{$opt}->%*};
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'};
313 $props->{digest
} = get_standard_option
('pve-config-digest');
316 type
=> 'string', format
=> 'pve-configid-list',
317 description
=> "A list of settings you want to delete.",
324 additionalProperties
=> 0,
325 properties
=> $props,
329 # the %param hash controls some behavior of the section config, currently the following options are
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.
336 my ($class, %param) = @_;
338 my $property_isolation = $param{property_isolation
};
340 my $pdata = $class->private();
342 foreach my $k (qw(options plugins plugindata propertyList isolatedPropertyList)) {
343 $pdata->{$k} = {} if !$pdata->{$k};
346 my $plugins = $pdata->{plugins
};
347 my $propertyList = $pdata->{propertyList
};
348 my $isolatedPropertyList = $pdata->{isolatedPropertyList
};
350 foreach my $type (keys %$plugins) {
351 my $props = $plugins->{$type}->properties();
352 foreach my $p (keys %$props) {
354 if ($property_isolation) {
355 $res = $isolatedPropertyList->{$type}->{$p} = {};
357 die "duplicate property '$p'" if defined($propertyList->{$p});
358 $res = $propertyList->{$p} = {};
360 my $data = $props->{$p};
361 for my $a (keys %$data) {
362 $res->{$a} = $data->{$a};
364 $res->{optional
} = 1;
368 foreach my $type (keys %$plugins) {
369 my $opts = $plugins->{$type}->options();
370 foreach my $p (keys %$opts) {
372 if ($property_isolation) {
373 $prop = $isolatedPropertyList->{$type}->{$p};
375 $prop //= $propertyList->{$p};
376 die "undefined property '$p'" if !$prop;
379 # automatically the properties to options (if not specified explicitly)
380 if ($property_isolation) {
381 foreach my $p (keys $isolatedPropertyList->{$type}->%*) {
384 $opts->{$p}->{optional
} = 1 if $isolatedPropertyList->{$type}->{$p}->{optional
};
388 $pdata->{options
}->{$type} = $opts;
391 $propertyList->{type
}->{type
} = 'string';
392 $propertyList->{type
}->{enum
} = [sort keys %$plugins];
396 my ($class, $type) = @_;
398 croak
"cannot lookup undefined type!" if !defined($type);
400 my $pdata = $class->private();
401 my $plugin = $pdata->{plugins
}->{$type};
403 die "unknown section type '$type'\n" if !$plugin;
411 my $pdata = $class->private();
413 return [ sort keys %{$pdata->{plugins
}} ];
417 my ($class, $type, $key, $value) = @_;
423 my ($class, $type, $key, $value) = @_;
429 my ($class, $type, $key, $value, $storeid, $skipSchemaCheck) = @_;
431 my $pdata = $class->private();
433 return $value if $key eq 'type' && $type eq $value;
435 my $opts = $pdata->{options
}->{$type};
436 die "unknown section type '$type'\n" if !$opts;
438 die "unexpected property '$key'\n" if !defined($opts->{$key});
440 my $schema = $class->get_property_schema($type, $key);
441 die "unknown property type\n" if !$schema;
443 my $ct = $schema->{type
};
445 $value = 1 if $ct eq 'boolean' && !defined($value);
447 die "got undefined value\n" if !defined($value);
449 die "property contains a line feed\n" if $value =~ m/[\n\r]/;
451 if (!$skipSchemaCheck) {
454 my $checkschema = $schema;
456 if ($ct eq 'array') {
457 die "no item schema for array" if !defined($schema->{items
});
458 $checkschema = $schema->{items
};
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";
469 if ($ct eq 'boolean' || $ct eq 'integer' || $ct eq 'number') {
470 return $value + 0; # convert to number
476 sub parse_section_header
{
477 my ($class, $line) = @_;
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);
488 sub format_section_header
{
489 my ($class, $type, $sectionId, $scfg, $done_hash) = @_;
491 return "$type: $sectionId\n";
494 sub get_property_schema
{
495 my ($class, $type, $key) = @_;
497 my $pdata = $class->private();
498 my $opts = $pdata->{options
}->{$type};
501 if ($class->has_isolated_properties()) {
502 $schema = $pdata->{isolatedPropertyList
}->{$type}->{$key};
504 $schema //= $pdata->{propertyList
}->{$key};
510 my ($class, $filename, $raw, $allow_unknown) = @_;
512 my $pdata = $class->private();
517 $raw = '' if !defined($raw);
519 my $digest = Digest
::SHA
::sha1_hex
($raw);
524 my @lines = split(/\n/, $raw);
526 while (defined(my $line = shift @lines)) {
528 return $line if ($line !~ /^\s*#/);
533 my ($type, $key) = @_;
535 my $schema = $class->get_property_schema($type, $key);
536 die "unknown property type\n" if !$schema;
538 return $schema->{type
} eq 'array';
543 my $line = $nextline->();
546 my $errprefix = "file $filename line $lineno";
548 my ($type, $sectionId, $errmsg, $config) = $class->parse_section_header($line);
558 warn "$errprefix (skip section '$sectionId'): $errmsg\n";
561 warn "$errprefix (skip section '$sectionId'): missing type - internal error\n";
563 if (!($plugin = $pdata->{plugins
}->{$type})) {
564 if ($allow_unknown) {
568 warn "$errprefix (skip section '$sectionId'): unsupported type '$type'\n";
573 while ($line = $nextline->()) {
574 next if $skip; # skip
576 $errprefix = "file $filename line $lineno";
578 if ($line =~ m/^\s+(\S+)(\s+(.*\S))?\s*$/) {
579 my ($k, $v) = ($1, $3);
583 if (!defined($config->{$k})) {
586 if (!ref($config->{$k})) {
587 $config->{$k} = [$config->{$k}];
589 push $config->{$k}->@*, $v;
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;
596 die "duplicate attribute\n" if defined($config->{$k});
597 $v = $plugin->check_value($type, $k, $v, $sectionId);
602 warn "$errprefix (section '$sectionId') - unable to parse value of '$k': $err";
604 context
=> $errprefix,
605 section
=> $sectionId,
612 warn "$errprefix (section '$sectionId') - ignore config line: $line\n";
617 $config->{type
} = $type;
618 $ids->{$sectionId} = $config;
619 $order->{$sectionId} = $pri++;
620 } elsif (!$skip && $type && $plugin && $config) {
621 $config->{type
} = $type;
623 $config = eval { $config = $plugin->check_config($sectionId, $config, 1, 1); };
624 warn "$errprefix (skip section '$sectionId'): $@" if $@;
626 $ids->{$sectionId} = $config;
627 $order->{$sectionId} = $pri++;
631 warn "$errprefix - ignore config line: $line\n";
640 $cfg->{errors
} = $errors if scalar(@$errors) > 0;
646 my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
648 my $type = $class->type();
649 my $pdata = $class->private();
650 my $opts = $pdata->{options
}->{$type};
652 my $settings = { type
=> $type };
654 foreach my $k (keys %$config) {
655 my $value = $config->{$k};
657 die "can't change value of fixed parameter '$k'\n"
658 if !$create && defined($opts->{$k}) && $opts->{$k}->{fixed
};
660 if (defined($value)) {
661 my $tmp = $class->check_value($type, $k, $value, $sectionId, $skipSchemaCheck);
662 $settings->{$k} = $class->decode_value($type, $k, $tmp);
664 die "got undefined value for option '$k'\n";
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});
680 my $format_config_line = sub {
681 my ($schema, $key, $value) = @_;
683 my $ct = $schema->{type
};
685 die "property '$key' contains a line feed\n"
686 if ($key =~ m/[\n\r]/) || ($value =~ m/[\n\r]/);
688 if ($ct eq 'boolean') {
689 return "\t$key " . ($value ?
1 : 0) . "\n"
691 } elsif ($ct eq 'array') {
692 die "property '$key' is not an array" if ref($value) ne 'ARRAY';
694 for my $line ($value->@*) {
695 $result .= "\t$key $line\n" if $value ne '';
699 return "\t$key $value\n" if "$value" ne '';
704 my ($class, $filename, $cfg, $allow_unknown) = @_;
706 my $pdata = $class->private();
710 my $ids = $cfg->{ids
};
711 my $order = $cfg->{order
};
714 foreach my $sectionId (keys %$ids) {
715 my $pri = $order->{$sectionId};
716 $maxpri = $pri if $pri && $pri > $maxpri;
718 foreach my $sectionId (keys %$ids) {
719 if (!defined ($order->{$sectionId})) {
720 $order->{$sectionId} = ++$maxpri;
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
};
730 die "unknown section type '$type'\n" if !$opts && !$allow_unknown;
734 my $data = $class->format_section_header($type, $sectionId, $scfg, $done_hash);
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;
744 if (defined($ref) && $ref eq 'ARRAY') {
745 $data .= "\t$k $_\n" for $v->@*;
747 $data .= "\t$k $v\n";
755 if ($scfg->{comment
} && !$done_hash->{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);
762 $data .= "\tdisable\n" if $scfg->{disable
} && !$done_hash->{disable
};
764 $done_hash->{comment
} = 1;
765 $done_hash->{disable
} = 1;
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;
773 die "section '$sectionId' - missing value for required option '$k'\n"
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);
780 foreach my $k (@option_keys) {
781 next if defined($done_hash->{$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);
795 sub assert_if_modified
{
796 my ($cfg, $digest) = @_;
798 PVE
::Tools
::assert_if_modified
($cfg->{digest
}, $digest);
801 sub delete_from_config
{
802 my ($config, $option_schema, $new_options, $to_delete) = @_;
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};