X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=src%2FPVE%2FSectionConfig.pm;h=a18e9d877850dd0e0fcca6e4967e2bc14c3dec8e;hb=HEAD;hp=a760459276249506e52054675eeeaa3a7bb9ce13;hpb=e1fbb779f7dc04be6e8925e790879b770adf0d2a;p=pve-common.git diff --git a/src/PVE/SectionConfig.pm b/src/PVE/SectionConfig.pm index a760459..a18e9d8 100644 --- a/src/PVE/SectionConfig.pm +++ b/src/PVE/SectionConfig.pm @@ -2,11 +2,73 @@ package PVE::SectionConfig; use strict; use warnings; + +use Carp; use Digest::SHA; + use PVE::Exception qw(raise_param_exc); use PVE::JSONSchema qw(get_standard_option); - -use Data::Dumper; +use PVE::Tools; + +# This package provides a way to have multiple (often similar) types of entries +# in the same config file, each in its own section, thus "Section Config". +# +# The intended structure is to have a single 'base' plugin that inherits from +# this class and provides meaningful defaults in its '$defaultData', e.g. a +# default list of the core properties in its propertyList (most often only 'id' +# and 'type') +# +# Each 'real' plugin then has it's own package that should inherit from the +# 'base' plugin and returns it's specific properties in the 'properties' method, +# its type in the 'type' method and all the known options, from both parent and +# itself, in the 'options' method. +# The options method can also be used to define if a property is 'optional' or +# 'fixed' (only settable on config entity-creation), for example: +# +# ```` +# sub options { +# return { +# 'some-optional-property' => { optional => 1 }, +# 'a-fixed-property' => { fixed => 1 }, +# 'a-required-but-not-fixed-property' => {}, +# }; +# } +# ``` +# +# 'fixed' options can be set on create, but not changed afterwards. +# +# To actually use it, you have to first register all the plugins and then init +# the 'base' plugin, like so: +# +# ``` +# use PVE::Dummy::Plugin1; +# use PVE::Dummy::Plugin2; +# use PVE::Dummy::BasePlugin; +# +# PVE::Dummy::Plugin1->register(); +# PVE::Dummy::Plugin2->register(); +# PVE::Dummy::BasePlugin->init(); +# ``` +# +# There are two modes for how properties are exposed, the default 'unified' +# mode and the 'isolated' mode. +# In the default unified mode, there is only a global list of properties +# which the plugins can use, so you cannot define the same property name twice +# in different plugins. The reason for this is to force the use of identical +# properties for multiple plugins. +# +# The second way is to use the 'isolated' mode, which can be achieved by +# calling init with `1` as its parameter like this: +# +# ``` +# PVE::Dummy::BasePlugin->init(property_isolation => 1); +# ``` +# +# With this, each plugin get's their own isolated list of properties which it +# can use. Note that in this mode, you only have to specify the property in the +# options method when it is either 'fixed' or comes from the global list of +# properties. All locally defined ones get automatically added to the schema +# for that plugin. my $defaultData = { options => {}, @@ -50,51 +112,126 @@ sub plugindata { return {}; } +sub has_isolated_properties { + my ($class) = @_; + + my $isolatedPropertyList = $class->private()->{isolatedPropertyList}; + + return defined($isolatedPropertyList) && scalar(keys $isolatedPropertyList->%*) > 0; +} + +my sub compare_property { + my ($a, $b, $skip_opts) = @_; + + my $merged = {$a->%*, $b->%*}; + delete $merged->{$_} for $skip_opts->@*; + + for my $opt (keys $merged->%*) { + return 0 if !PVE::Tools::is_deeply($a->{$opt}, $b->{$opt}); + } + + return 1; +}; + +my sub add_property { + my ($props, $key, $prop, $type) = @_; + + if (!defined($props->{$key})) { + $props->{$key} = $prop; + return; + } + + if (!defined($props->{$key}->{oneOf})) { + if (compare_property($props->{$key}, $prop, ['instance-types'])) { + push $props->{$key}->{'instance-types'}->@*, $type; + } else { + my $new_prop = delete $props->{$key}; + delete $new_prop->{'type-property'}; + delete $prop->{'type-property'}; + $props->{$key} = { + 'type-property' => 'type', + oneOf => [ + $new_prop, + $prop, + ], + }; + } + } else { + for my $existing_prop ($props->{$key}->{oneOf}->@*) { + if (compare_property($existing_prop, $prop, ['instance-types', 'type-property'])) { + push $existing_prop->{'instance-types'}->@*, $type; + return; + } + } + + push $props->{$key}->{oneOf}->@*, $prop; + } +}; + sub createSchema { - my ($class, $skip_type) = @_; + my ($class, $skip_type, $base) = @_; my $pdata = $class->private(); my $propertyList = $pdata->{propertyList}; my $plugins = $pdata->{plugins}; - my $props = {}; - - my $copy_property = sub { - my ($src) = @_; + my $props = $base || {}; - my $res = {}; - foreach my $k (keys %$src) { - $res->{$k} = $src->{$k}; - } - - return $res; - }; + if (!$class->has_isolated_properties()) { + foreach my $p (keys %$propertyList) { + next if $skip_type && $p eq 'type'; - foreach my $p (keys %$propertyList) { - next if $skip_type && $p eq 'type'; + if (!$propertyList->{$p}->{optional}) { + $props->{$p} = $propertyList->{$p}; + next; + } - if (!$propertyList->{$p}->{optional}) { - $props->{$p} = $propertyList->{$p}; - next; - } + my $required = 1; - my $required = 1; + my $copts = $class->options(); + $required = 0 if defined($copts->{$p}) && $copts->{$p}->{optional}; - my $copts = $class->options(); - $required = 0 if defined($copts->{$p}) && $copts->{$p}->{optional}; + foreach my $t (keys %$plugins) { + my $opts = $pdata->{options}->{$t} || {}; + $required = 0 if !defined($opts->{$p}) || $opts->{$p}->{optional}; + } - foreach my $t (keys %$plugins) { - my $opts = $pdata->{options}->{$t} || {}; - $required = 0 if !defined($opts->{$p}) || $opts->{$p}->{optional}; + if ($required) { + # make a copy, because we modify the optional property + my $res = {$propertyList->{$p}->%*}; # shallow copy + $res->{optional} = 0; + $props->{$p} = $res; + } else { + $props->{$p} = $propertyList->{$p}; + } } - - if ($required) { - # make a copy, because we modify the optional property - my $res = &$copy_property($propertyList->{$p}); - $res->{optional} = 0; - $props->{$p} = $res; - } else { - $props->{$p} = $propertyList->{$p}; + } else { + for my $type (sort keys %$plugins) { + my $opts = $pdata->{options}->{$type} || {}; + for my $key (sort keys $opts->%*) { + my $schema = $class->get_property_schema($type, $key); + my $prop = {$schema->%*}; + $prop->{'instance-types'} = [$type]; + $prop->{'type-property'} = 'type'; + $prop->{optional} = 1 if $opts->{$key}->{optional}; + + add_property($props, $key, $prop, $type); + } + } + # add remaining global properties + for my $opt (keys $propertyList->%*) { + next if $props->{$opt}; + $props->{$opt} = {$propertyList->{$opt}->%*}; + } + for my $opt (keys $props->%*) { + if (my $necessaryTypes = $props->{$opt}->{'instance-types'}) { + if ($necessaryTypes->@* == scalar(keys $plugins->%*)) { + delete $props->{$opt}->{'instance-types'}; + delete $props->{$opt}->{'type-property'}; + } else { + $props->{$opt}->{optional} = 1; + } + } } } @@ -106,40 +243,71 @@ sub createSchema { } sub updateSchema { - my ($class, $single_class) = @_; + my ($class, $single_class, $base) = @_; my $pdata = $class->private(); my $propertyList = $pdata->{propertyList}; my $plugins = $pdata->{plugins}; - my $props = {}; + my $props = $base || {}; - my $filter_type = $class->type() if $single_class; + my $filter_type = $single_class ? $class->type() : undef; - foreach my $p (keys %$propertyList) { - next if $p eq 'type'; + if (!$class->has_isolated_properties()) { + foreach my $p (keys %$propertyList) { + next if $p eq 'type'; - my $copts = $class->options(); + my $copts = $class->options(); - next if defined($filter_type) && !defined($copts->{$p}); + next if defined($filter_type) && !defined($copts->{$p}); - if (!$propertyList->{$p}->{optional}) { - $props->{$p} = $propertyList->{$p}; - next; - } + if (!$propertyList->{$p}->{optional}) { + $props->{$p} = $propertyList->{$p}; + next; + } - my $modifyable = 0; + my $modifyable = 0; - $modifyable = 1 if defined($copts->{$p}) && !$copts->{$p}->{fixed}; + $modifyable = 1 if defined($copts->{$p}) && !$copts->{$p}->{fixed}; - foreach my $t (keys %$plugins) { - my $opts = $pdata->{options}->{$t} || {}; - next if !defined($opts->{$p}); - $modifyable = 1 if !$opts->{$p}->{fixed}; + foreach my $t (keys %$plugins) { + my $opts = $pdata->{options}->{$t} || {}; + next if !defined($opts->{$p}); + $modifyable = 1 if !$opts->{$p}->{fixed}; + } + next if !$modifyable; + + $props->{$p} = $propertyList->{$p}; + } + } else { + for my $type (sort keys %$plugins) { + my $opts = $pdata->{options}->{$type} || {}; + for my $key (sort keys $opts->%*) { + next if $opts->{$key}->{fixed}; + + my $schema = $class->get_property_schema($type, $key); + my $prop = {$schema->%*}; + $prop->{'instance-types'} = [$type]; + $prop->{'type-property'} = 'type'; + $prop->{optional} = 1; + + add_property($props, $key, $prop, $type); + } } - next if !$modifyable; - $props->{$p} = $propertyList->{$p}; + for my $opt (keys $propertyList->%*) { + next if $props->{$opt}; + $props->{$opt} = {$propertyList->{$opt}->%*}; + } + + for my $opt (keys $props->%*) { + if (my $necessaryTypes = $props->{$opt}->{'instance-types'}) { + if ($necessaryTypes->@* == scalar(keys $plugins->%*)) { + delete $props->{$opt}->{'instance-types'}; + delete $props->{$opt}->{'type-property'}; + } + } + } } $props->{digest} = get_standard_option('pve-config-digest'); @@ -158,23 +326,37 @@ sub updateSchema { }; } +# the %param hash controls some behavior of the section config, currently the following options are +# understood: +# +# - property_isolation: if set, each child-plugin has a fully isolated property (schema) namespace. +# By default this is off, meaning all child-plugins share the schema of properties with the same +# name. Normally one wants to use oneOf schema's when enabling isolation. sub init { - my ($class) = @_; + my ($class, %param) = @_; + + my $property_isolation = $param{property_isolation}; my $pdata = $class->private(); - foreach my $k (qw(options plugins plugindata propertyList)) { + foreach my $k (qw(options plugins plugindata propertyList isolatedPropertyList)) { $pdata->{$k} = {} if !$pdata->{$k}; } my $plugins = $pdata->{plugins}; my $propertyList = $pdata->{propertyList}; + my $isolatedPropertyList = $pdata->{isolatedPropertyList}; foreach my $type (keys %$plugins) { my $props = $plugins->{$type}->properties(); foreach my $p (keys %$props) { - die "duplicate property '$p'" if defined($propertyList->{$p}); - my $res = $propertyList->{$p} = {}; + my $res; + if ($property_isolation) { + $res = $isolatedPropertyList->{$type}->{$p} = {}; + } else { + die "duplicate property '$p'" if defined($propertyList->{$p}); + $res = $propertyList->{$p} = {}; + } my $data = $props->{$p}; for my $a (keys %$data) { $res->{$a} = $data->{$a}; @@ -186,8 +368,23 @@ sub init { foreach my $type (keys %$plugins) { my $opts = $plugins->{$type}->options(); foreach my $p (keys %$opts) { - die "undefined property '$p'" if !$propertyList->{$p}; + my $prop; + if ($property_isolation) { + $prop = $isolatedPropertyList->{$type}->{$p}; + } + $prop //= $propertyList->{$p}; + die "undefined property '$p'" if !$prop; } + + # automatically the properties to options (if not specified explicitly) + if ($property_isolation) { + foreach my $p (keys $isolatedPropertyList->{$type}->%*) { + next if $opts->{$p}; + $opts->{$p} = {}; + $opts->{$p}->{optional} = 1 if $isolatedPropertyList->{$type}->{$p}->{optional}; + } + } + $pdata->{options}->{$type} = $opts; } @@ -198,6 +395,8 @@ sub init { sub lookup { my ($class, $type) = @_; + croak "cannot lookup undefined type!" if !defined($type); + my $pdata = $class->private(); my $plugin = $pdata->{plugins}->{$type}; @@ -238,7 +437,7 @@ sub check_value { die "unexpected property '$key'\n" if !defined($opts->{$key}); - my $schema = $pdata->{propertyList}->{$key}; + my $schema = $class->get_property_schema($type, $key); die "unknown property type\n" if !$schema; my $ct = $schema->{type}; @@ -251,7 +450,15 @@ sub check_value { if (!$skipSchemaCheck) { my $errors = {}; - PVE::JSONSchema::check_prop($value, $schema, '', $errors); + + my $checkschema = $schema; + + if ($ct eq 'array') { + die "no item schema for array" if !defined($schema->{items}); + $checkschema = $schema->{items}; + } + + PVE::JSONSchema::check_prop($value, $checkschema, '', $errors); if (scalar(keys %$errors)) { die "$errors->{$key}\n" if $errors->{$key}; die "$errors->{_root}\n" if $errors->{_root}; @@ -284,9 +491,23 @@ sub format_section_header { return "$type: $sectionId\n"; } +sub get_property_schema { + my ($class, $type, $key) = @_; + + my $pdata = $class->private(); + my $opts = $pdata->{options}->{$type}; + + my $schema; + if ($class->has_isolated_properties()) { + $schema = $pdata->{isolatedPropertyList}->{$type}->{$key}; + } + $schema //= $pdata->{propertyList}->{$key}; + + return $schema; +} sub parse_config { - my ($class, $filename, $raw) = @_; + my ($class, $filename, $raw, $allow_unknown) = @_; my $pdata = $class->private(); @@ -308,6 +529,16 @@ sub parse_config { } }; + my $is_array = sub { + my ($type, $key) = @_; + + my $schema = $class->get_property_schema($type, $key); + die "unknown property type\n" if !$schema; + + return $schema->{type} eq 'array'; + }; + + my $errors = []; while (@lines) { my $line = $nextline->(); next if !$line; @@ -316,26 +547,31 @@ sub parse_config { my ($type, $sectionId, $errmsg, $config) = $class->parse_section_header($line); if ($config) { - my $ignore = 0; + my $skip = 0; + my $unknown = 0; my $plugin; if ($errmsg) { - $ignore = 1; + $skip = 1; chomp $errmsg; warn "$errprefix (skip section '$sectionId'): $errmsg\n"; } elsif (!$type) { - $ignore = 1; + $skip = 1; warn "$errprefix (skip section '$sectionId'): missing type - internal error\n"; } else { if (!($plugin = $pdata->{plugins}->{$type})) { - $ignore = 1; - warn "$errprefix (skip section '$sectionId'): unsupported type '$type'\n"; + if ($allow_unknown) { + $unknown = 1; + } else { + $skip = 1; + warn "$errprefix (skip section '$sectionId'): unsupported type '$type'\n"; + } } } - while ($line = &$nextline()) { - next if $ignore; # skip + while ($line = $nextline->()) { + next if $skip; # skip $errprefix = "file $filename line $lineno"; @@ -343,20 +579,51 @@ sub parse_config { my ($k, $v) = ($1, $3); eval { - die "duplicate attribute\n" if defined($config->{$k}); - $config->{$k} = $plugin->check_value($type, $k, $v, $sectionId); + if ($unknown) { + if (!defined($config->{$k})) { + $config->{$k} = $v; + } else { + if (!ref($config->{$k})) { + $config->{$k} = [$config->{$k}]; + } + push $config->{$k}->@*, $v; + } + } elsif ($is_array->($type, $k)) { + $v = $plugin->check_value($type, $k, $v, $sectionId); + $config->{$k} = [] if !defined($config->{$k}); + push $config->{$k}->@*, $v; + } else { + die "duplicate attribute\n" if defined($config->{$k}); + $v = $plugin->check_value($type, $k, $v, $sectionId); + $config->{$k} = $v; + } }; - warn "$errprefix (section '$sectionId') - unable to parse value of '$k': $@" if $@; + if (my $err = $@) { + warn "$errprefix (section '$sectionId') - unable to parse value of '$k': $err"; + push @$errors, { + context => $errprefix, + section => $sectionId, + key => $k, + err => $err, + }; + } } else { warn "$errprefix (section '$sectionId') - ignore config line: $line\n"; } } - if (!$ignore && $type && $plugin && $config) { + if ($unknown) { $config->{type} = $type; - eval { $ids->{$sectionId} = $plugin->check_config($sectionId, $config, 1, 1); }; - warn "$errprefix (skip section '$sectionId'): $@" if $@; + $ids->{$sectionId} = $config; + $order->{$sectionId} = $pri++; + } elsif (!$skip && $type && $plugin && $config) { + $config->{type} = $type; + if (!$unknown) { + $config = eval { $config = $plugin->check_config($sectionId, $config, 1, 1); }; + warn "$errprefix (skip section '$sectionId'): $@" if $@; + } + $ids->{$sectionId} = $config; $order->{$sectionId} = $pri++; } @@ -365,8 +632,12 @@ sub parse_config { } } - - my $cfg = { ids => $ids, order => $order, digest => $digest}; + my $cfg = { + ids => $ids, + order => $order, + digest => $digest + }; + $cfg->{errors} = $errors if scalar(@$errors) > 0; return $cfg; } @@ -417,16 +688,22 @@ my $format_config_line = sub { if ($ct eq 'boolean') { return "\t$key " . ($value ? 1 : 0) . "\n" if defined($value); + } elsif ($ct eq 'array') { + die "property '$key' is not an array" if ref($value) ne 'ARRAY'; + my $result = ''; + for my $line ($value->@*) { + $result .= "\t$key $line\n" if $value ne ''; + } + return $result; } else { return "\t$key $value\n" if "$value" ne ''; } }; sub write_config { - my ($class, $filename, $cfg) = @_; + my ($class, $filename, $cfg, $allow_unknown) = @_; my $pdata = $class->private(); - my $propertyList = $pdata->{propertyList}; my $out = ''; @@ -448,16 +725,38 @@ sub write_config { my $scfg = $ids->{$sectionId}; my $type = $scfg->{type}; my $opts = $pdata->{options}->{$type}; + my $global_opts = $pdata->{options}->{__global}; - die "unknown section type '$type'\n" if !$opts; + die "unknown section type '$type'\n" if !$opts && !$allow_unknown; my $done_hash = {}; my $data = $class->format_section_header($type, $sectionId, $scfg, $done_hash); + + if (!$opts && $allow_unknown) { + $done_hash->{type} = 1; + my @first = exists($scfg->{comment}) ? ('comment') : (); + for my $k (@first, sort keys %$scfg) { + next if defined($done_hash->{$k}); + $done_hash->{$k} = 1; + my $v = $scfg->{$k}; + my $ref = ref($v); + if (defined($ref) && $ref eq 'ARRAY') { + $data .= "\t$k $_\n" for $v->@*; + } else { + $data .= "\t$k $v\n"; + } + } + $out .= "$data\n"; + next; + } + + if ($scfg->{comment} && !$done_hash->{comment}) { my $k = 'comment'; my $v = $class->encode_value($type, $k, $scfg->{$k}); - $data .= &$format_config_line($propertyList->{$k}, $k, $v); + my $prop = $class->get_property_schema($type, $k); + $data .= &$format_config_line($prop, $k, $v); } $data .= "\tdisable\n" if $scfg->{disable} && !$done_hash->{disable}; @@ -474,7 +773,8 @@ sub write_config { die "section '$sectionId' - missing value for required option '$k'\n" if !defined ($v); $v = $class->encode_value($type, $k, $v); - $data .= &$format_config_line($propertyList->{$k}, $k, $v); + my $prop = $class->get_property_schema($type, $k); + $data .= &$format_config_line($prop, $k, $v); } foreach my $k (@option_keys) { @@ -482,7 +782,8 @@ sub write_config { my $v = $scfg->{$k}; next if !defined($v); $v = $class->encode_value($type, $k, $v); - $data .= &$format_config_line($propertyList->{$k}, $k, $v); + my $prop = $class->get_property_schema($type, $k); + $data .= &$format_config_line($prop, $k, $v); } $out .= "$data\n"; @@ -497,4 +798,19 @@ sub assert_if_modified { PVE::Tools::assert_if_modified($cfg->{digest}, $digest); } +sub delete_from_config { + my ($config, $option_schema, $new_options, $to_delete) = @_; + + for my $k ($to_delete->@*) { + my $d = $option_schema->{$k} || die "no such option '$k'\n"; + die "unable to delete required option '$k'\n" if !$d->{optional}; + die "unable to delete fixed option '$k'\n" if $d->{fixed}; + die "cannot set and delete property '$k' at the same time!\n" + if defined($new_options->{$k}); + delete $config->{$k}; + } + + return $config; +} + 1;