]>
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(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,
330 my ($class, $property_isolation) = @_;
332 my $pdata = $class->private();
334 foreach my $k (qw(options plugins plugindata propertyList isolatedPropertyList)) {
335 $pdata->{$k} = {} if !$pdata->{$k};
338 my $plugins = $pdata->{plugins
};
339 my $propertyList = $pdata->{propertyList
};
340 my $isolatedPropertyList = $pdata->{isolatedPropertyList
};
342 foreach my $type (keys %$plugins) {
343 my $props = $plugins->{$type}->properties();
344 foreach my $p (keys %$props) {
346 if ($property_isolation) {
347 $res = $isolatedPropertyList->{$type}->{$p} = {};
349 die "duplicate property '$p'" if defined($propertyList->{$p});
350 $res = $propertyList->{$p} = {};
352 my $data = $props->{$p};
353 for my $a (keys %$data) {
354 $res->{$a} = $data->{$a};
356 $res->{optional
} = 1;
360 foreach my $type (keys %$plugins) {
361 my $opts = $plugins->{$type}->options();
362 foreach my $p (keys %$opts) {
364 if ($property_isolation) {
365 $prop = $isolatedPropertyList->{$type}->{$p};
367 $prop //= $propertyList->{$p};
368 die "undefined property '$p'" if !$prop;
371 # automatically the properties to options (if not specified explicitly)
372 if ($property_isolation) {
373 foreach my $p (keys $isolatedPropertyList->{$type}->%*) {
376 $opts->{$p}->{optional
} = 1 if $isolatedPropertyList->{$type}->{$p}->{optional
};
380 $pdata->{options
}->{$type} = $opts;
383 $propertyList->{type
}->{type
} = 'string';
384 $propertyList->{type
}->{enum
} = [sort keys %$plugins];
388 my ($class, $type) = @_;
390 croak
"cannot lookup undefined type!" if !defined($type);
392 my $pdata = $class->private();
393 my $plugin = $pdata->{plugins
}->{$type};
395 die "unknown section type '$type'\n" if !$plugin;
403 my $pdata = $class->private();
405 return [ sort keys %{$pdata->{plugins
}} ];
409 my ($class, $type, $key, $value) = @_;
415 my ($class, $type, $key, $value) = @_;
421 my ($class, $type, $key, $value, $storeid, $skipSchemaCheck) = @_;
423 my $pdata = $class->private();
425 return $value if $key eq 'type' && $type eq $value;
427 my $opts = $pdata->{options
}->{$type};
428 die "unknown section type '$type'\n" if !$opts;
430 die "unexpected property '$key'\n" if !defined($opts->{$key});
432 my $schema = $class->get_property_schema($type, $key);
433 die "unknown property type\n" if !$schema;
435 my $ct = $schema->{type
};
437 $value = 1 if $ct eq 'boolean' && !defined($value);
439 die "got undefined value\n" if !defined($value);
441 die "property contains a line feed\n" if $value =~ m/[\n\r]/;
443 if (!$skipSchemaCheck) {
446 my $checkschema = $schema;
448 if ($ct eq 'array') {
449 die "no item schema for array" if !defined($schema->{items
});
450 $checkschema = $schema->{items
};
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";
461 if ($ct eq 'boolean' || $ct eq 'integer' || $ct eq 'number') {
462 return $value + 0; # convert to number
468 sub parse_section_header
{
469 my ($class, $line) = @_;
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);
480 sub format_section_header
{
481 my ($class, $type, $sectionId, $scfg, $done_hash) = @_;
483 return "$type: $sectionId\n";
486 sub get_property_schema
{
487 my ($class, $type, $key) = @_;
489 my $pdata = $class->private();
490 my $opts = $pdata->{options
}->{$type};
493 if ($class->has_isolated_properties()) {
494 $schema = $pdata->{isolatedPropertyList
}->{$type}->{$key};
496 $schema //= $pdata->{propertyList
}->{$key};
502 my ($class, $filename, $raw, $allow_unknown) = @_;
504 my $pdata = $class->private();
509 $raw = '' if !defined($raw);
511 my $digest = Digest
::SHA
::sha1_hex
($raw);
516 my @lines = split(/\n/, $raw);
518 while (defined(my $line = shift @lines)) {
520 return $line if ($line !~ /^\s*#/);
525 my ($type, $key) = @_;
527 my $schema = $class->get_property_schema($type, $key);
528 die "unknown property type\n" if !$schema;
530 return $schema->{type
} eq 'array';
535 my $line = $nextline->();
538 my $errprefix = "file $filename line $lineno";
540 my ($type, $sectionId, $errmsg, $config) = $class->parse_section_header($line);
550 warn "$errprefix (skip section '$sectionId'): $errmsg\n";
553 warn "$errprefix (skip section '$sectionId'): missing type - internal error\n";
555 if (!($plugin = $pdata->{plugins
}->{$type})) {
556 if ($allow_unknown) {
560 warn "$errprefix (skip section '$sectionId'): unsupported type '$type'\n";
565 while ($line = $nextline->()) {
566 next if $skip; # skip
568 $errprefix = "file $filename line $lineno";
570 if ($line =~ m/^\s+(\S+)(\s+(.*\S))?\s*$/) {
571 my ($k, $v) = ($1, $3);
575 if (!defined($config->{$k})) {
578 if (!ref($config->{$k})) {
579 $config->{$k} = [$config->{$k}];
581 push $config->{$k}->@*, $v;
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;
588 die "duplicate attribute\n" if defined($config->{$k});
589 $v = $plugin->check_value($type, $k, $v, $sectionId);
594 warn "$errprefix (section '$sectionId') - unable to parse value of '$k': $err";
596 context
=> $errprefix,
597 section
=> $sectionId,
604 warn "$errprefix (section '$sectionId') - ignore config line: $line\n";
609 $config->{type
} = $type;
610 $ids->{$sectionId} = $config;
611 $order->{$sectionId} = $pri++;
612 } elsif (!$skip && $type && $plugin && $config) {
613 $config->{type
} = $type;
615 $config = eval { $config = $plugin->check_config($sectionId, $config, 1, 1); };
616 warn "$errprefix (skip section '$sectionId'): $@" if $@;
618 $ids->{$sectionId} = $config;
619 $order->{$sectionId} = $pri++;
623 warn "$errprefix - ignore config line: $line\n";
632 $cfg->{errors
} = $errors if scalar(@$errors) > 0;
638 my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
640 my $type = $class->type();
641 my $pdata = $class->private();
642 my $opts = $pdata->{options
}->{$type};
644 my $settings = { type
=> $type };
646 foreach my $k (keys %$config) {
647 my $value = $config->{$k};
649 die "can't change value of fixed parameter '$k'\n"
650 if !$create && defined($opts->{$k}) && $opts->{$k}->{fixed
};
652 if (defined($value)) {
653 my $tmp = $class->check_value($type, $k, $value, $sectionId, $skipSchemaCheck);
654 $settings->{$k} = $class->decode_value($type, $k, $tmp);
656 die "got undefined value for option '$k'\n";
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});
672 my $format_config_line = sub {
673 my ($schema, $key, $value) = @_;
675 my $ct = $schema->{type
};
677 die "property '$key' contains a line feed\n"
678 if ($key =~ m/[\n\r]/) || ($value =~ m/[\n\r]/);
680 if ($ct eq 'boolean') {
681 return "\t$key " . ($value ?
1 : 0) . "\n"
683 } elsif ($ct eq 'array') {
684 die "property '$key' is not an array" if ref($value) ne 'ARRAY';
686 for my $line ($value->@*) {
687 $result .= "\t$key $line\n" if $value ne '';
691 return "\t$key $value\n" if "$value" ne '';
696 my ($class, $filename, $cfg, $allow_unknown) = @_;
698 my $pdata = $class->private();
702 my $ids = $cfg->{ids
};
703 my $order = $cfg->{order
};
706 foreach my $sectionId (keys %$ids) {
707 my $pri = $order->{$sectionId};
708 $maxpri = $pri if $pri && $pri > $maxpri;
710 foreach my $sectionId (keys %$ids) {
711 if (!defined ($order->{$sectionId})) {
712 $order->{$sectionId} = ++$maxpri;
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
};
722 die "unknown section type '$type'\n" if !$opts && !$allow_unknown;
726 my $data = $class->format_section_header($type, $sectionId, $scfg, $done_hash);
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;
736 if (defined($ref) && $ref eq 'ARRAY') {
737 $data .= "\t$k $_\n" for $v->@*;
739 $data .= "\t$k $v\n";
747 if ($scfg->{comment
} && !$done_hash->{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);
754 $data .= "\tdisable\n" if $scfg->{disable
} && !$done_hash->{disable
};
756 $done_hash->{comment
} = 1;
757 $done_hash->{disable
} = 1;
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;
765 die "section '$sectionId' - missing value for required option '$k'\n"
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);
772 foreach my $k (@option_keys) {
773 next if defined($done_hash->{$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);
787 sub assert_if_modified
{
788 my ($cfg, $digest) = @_;
790 PVE
::Tools
::assert_if_modified
($cfg->{digest
}, $digest);
793 sub delete_from_config
{
794 my ($config, $option_schema, $new_options, $to_delete) = @_;
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};