diff --git a/src/PVE/SectionConfig.pm b/src/PVE/SectionConfig.pm index a18e9d8..99ee348 100644 --- a/src/PVE/SectionConfig.pm +++ b/src/PVE/SectionConfig.pm @@ -10,65 +10,102 @@ use PVE::Exception qw(raise_param_exc); use PVE::JSONSchema qw(get_standard_option); 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. +=pod + +=head1 NAME + +SectionConfig + +=head1 DESCRIPTION + +This package provides a way to have multiple (often similar) types of entries +in the same config file, each in its own section, thus I
. + +Under the hood, this package automatically creates and manages a matching +I for one's plugin architecture that is used to represent data +that is read from and written to the config file. + +Where this config file is located, as well as its permissions and other related +things, is up to the plugin author and is not handled by C +at all. + +=head1 USAGE + +The intended structure is to have a single I that inherits from +this class and provides meaningful defaults in its C<$defaultData>, such as a +default list of core C I. The I is +thus very similar to an I. + +Each I is then defined in its own package that should inherit +from the I and defines which I it itself provides and +uses, as well as which I it uses from the I. + +The methods that need to be implemented are annotated in the L section +below. + + ┌─────────────────┐ + │ SectionConfig │ + └────────┬────────┘ + │ + │ + │ + ┌────────▼────────┐ + │ BasePlugin │ + └────────┬────────┘ + │ + ┌─────────┴─────────┐ + │ │ + ┌────────▼────────┐ ┌────────▼────────┐ + │ConcretePluginFoo│ │ConcretePluginBar│ + └─────────────────┘ └─────────────────┘ + +=head2 REGISTERING PLUGINS + +In order to actually be able to use plugins, they must first be I +and then I via the "base" plugin: + + use PVE::Example::BasePlugin; + use PVE::Example::PluginA; + use PVE::Example::PluginB; + + PVE::Example::PluginA->register(); + PVE::Example::PluginB->register(); + PVE::Example::BasePlugin->init(); + +=head2 MODES + +There are two modes for how I are exposed. + +=head3 unified mode (default) + +In this mode there is only a global list of I which the child +plugins can use. This has the consequence that it's not possible to define the +same property name more than once in different plugins. + +The reason behind this behaviour is to ensure that properties with the same +name don't behave in different ways, or in other words, to enforce the use of +identical properties for multiple plugins. + +=head3 isolated mode + +This mode can be used by calling C with an additional parameter: + + PVE::Example::BasePlugin->init(property_isolation => 1); + +With this mode each I gets its own isolated list of I, +or in other words, a fully isolated schema namespace. Normally one wants to use +C schemas when enabling isolation. + +Note that in this mode it's only necessary to specify a I in the +C method when it's either C or stems from the global list of +I. + +All locally defined I of a I are automatically added +to its schema. + +=head2 METHODS + +=cut my $defaultData = { options => {}, @@ -77,11 +114,85 @@ my $defaultData = { propertyList => {}, }; +=pod + +=head3 private + +B Must be implemented in the I. + + $data = PVE::Example::Plugin->private() + $data = $class->private() + +Getter for C-related private data. + +Most commonly this is used to simply retrieve the default I list of +one's plugin architecture, for example: + + use PVE::JSONSchema qw(get_standard_option); + + use base qw(PVE::SectionConfig); + + # [...] + + my $defaultData = { + propertyList => { + type => { + description => "Type of plugin." + }, + nodes => get_standard_option('pve-node-list', { + description => "List of nodes for which the plugin applies.", + optional => 1, + }), + disable => { + description => "Flag to disable the plugin.", + type => 'boolean', + optional => 1, + }, + 'max-foo-rate' => { + description => "Maximum 'foo' rate of the plugin. Use '-1' for unlimited.", + type => 'integer', + minimum => -1, + default => 42, + optional => 1, + }, + # [...] + }, + }; + + sub private { + return $defaultData; + } + +=cut + sub private { die "overwrite me"; return $defaultData; } +=pod + +=head3 register + + PVE::Example::Plugin->register() + +Used to register I. + +This method must be called on each child plugin before I the base +plugin. + +For example: + + use PVE::Example::BasePlugin; + use PVE::Example::PluginA; + use PVE::Example::PluginB; + + PVE::Example::PluginA->register(); + PVE::Example::PluginB->register(); + PVE::Example::BasePlugin->init(); + +=cut + sub register { my ($class) = @_; @@ -96,22 +207,127 @@ sub register { $pdata->{plugins}->{$type} = $class; } +=pod + +=head3 type + +B Must be implemented in I. + + $type = PVE::Example::Plugin->type() + $type = $class->type() + +Returns the I of a I, which is a I string. This is +used to identify the I. + +Should be overridden on I: + + sub type { + return "foo"; + } + +=cut + sub type { die "overwrite me"; } +=pod + +=head3 properties + +B Must be implemented in I. + + $props = PVE::Example::Plugin->properties() + $props = $class->properties() + +Returns the I specific to a I as a hash. + + sub properties() { + return { + path => { + description => "Path used to retrieve a 'foo'.", + type => 'string', + format => 'some-custom-format-handler-for-paths', + }, + is_bar = { + description => "Whether the 'foo' is 'bar' or not.", + type => 'boolean', + }, + bwlimit => get_standard_option('bwlimit'), + }; + } + +=cut + sub properties { return {}; } +=pod + +=head3 options + +B Must be implemented in I. + + $opts = PVE::Example::Plugin->options() + $opts = $class->options() + +This method is used to specify which I are actually configured for +a given I. More precisely, only the I that are +contained in the hash this method returns can be used. + +Additionally, it also allows to declare whether a property is C or +C. + + sub options { + return { + 'some-optional-property' => { optional => 1 }, + 'a-fixed-property' => { fixed => 1 }, + 'a-required-but-not-fixed-property' => {}, + }; + } + +C I are not required to be set. + +C I may only be set on creation of the config entity. + +=cut + sub options { return {}; } +=pod + +=head3 plugindata + +B Can be implemented in I. + + $plugindata = PVE::Example::Plugin->plugindata() + $plugindata = $class->plugindata() + +This method is used by plugin authors to provide any kind of data specific to +their plugin implementation and is otherwise not touched by C. + +This mostly exists for convenience and doesn't need to be implemented. + +=cut + sub plugindata { return {}; } +=pod + +=head3 has_isolated_properties + + $is_isolated = PVE::Example::Plugin->has_isolated_properties() + $is_isolated = $class->has_isolated_properties() + +Checks whether the plugin has isolated I (runs in isolated mode). + +=cut + sub has_isolated_properties { my ($class) = @_; @@ -168,6 +384,34 @@ my sub add_property { } }; +=pod + +=head3 createSchema + + $schema = PVE::Example::Plugin->($skip_type, $base) + $schema = $class->($skip_type, $base) + +Returns the C used for I instances of a +I. + +This schema may then be used as desired, for example as the definition of +parameters of an API handler (C). + +=over + +=item C<$skip_type> (optional) + +Can be set to C<1> if there's a I named "type" in the list of +default I that should be excluded from the generated schema. + +=item C<$base> (optional) + +The I to use per default. + +=back + +=cut + sub createSchema { my ($class, $skip_type, $base) = @_; @@ -242,6 +486,18 @@ sub createSchema { }; } +=pod + +=head3 updateSchema + +Returns the C used for I instances of a +I. + +This schema may then be used as desired, for example as the definition of +parameters of an API handler (C). + +=cut + sub updateSchema { my ($class, $single_class, $base) = @_; @@ -326,12 +582,22 @@ 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. +=pod + +=head3 init + + $base_plugin->init(); + $base_plugin->init(property_isolation => 1); + +This method is used to initialize all I that have been +I beforehand. + +Optionally, it is also possible to pass C as parameter in +order to activate I. See L in the package-level +documentation for more information. + +=cut + sub init { my ($class, %param) = @_; @@ -392,6 +658,18 @@ sub init { $propertyList->{type}->{enum} = [sort keys %$plugins]; } +=pod + +=head3 lookup + + $plugin = PVE::Example::BasePlugin->lookup($type) + $plugin = $class->lookup($type) + +Returns the I corresponding to the given C or dies if it +cannot be found. + +=cut + sub lookup { my ($class, $type) = @_; @@ -405,6 +683,17 @@ sub lookup { return $plugin; } +=pod + +=head3 lookup_types + + $types = PVE::Example::BasePlugin->lookup_types() + $types = $class->lookup_types() + +Returns a list of all I Cs. + +=cut + sub lookup_types { my ($class) = @_; @@ -413,18 +702,66 @@ sub lookup_types { return [ sort keys %{$pdata->{plugins}} ]; } +=pod + +=head3 decode_value + +B Can be implemented in the I. + + $decoded_value = PVE::Example::BasePlugin->decode_value($type, $key, $value) + $decoded_value = $class->($type, $key, $value) + +Called during C in order to convert values that have been read +from a C file which have been I beforehand by +C. + +Does nothing to C<$value> by default, but can be overridden in the I +in order to implement custom conversion behavior. + +=cut + sub decode_value { my ($class, $type, $key, $value) = @_; return $value; } +=pod + +=head3 encode_value + +B Can be implemented in the I. + + $encoded_value = PVE::Example::BasePlugin->encode_value($type, $key, $value) + $encoded_value = $class->($type, $key, $value) + +Called during C in order to convert values into a serializable +format. + +Does nothing to C<$value> by default, but can be overridden in the I +in order to implement custom conversion behavior. Usually one should also +override C in a matching manner. + +=cut + sub encode_value { my ($class, $type, $key, $value) = @_; return $value; } +=pod + +=head3 check_value + + $checked_value = PVE::Example::BasePlugin->check_value($type, $key, $value, $storeid, $skipSchemaCheck) + $checked_value = $class->check_value($type, $key, $value, $storeid, $skipSchemaCheck) + +Used internally to check if various invariants are upheld. It's best to not +override this. + +=cut + sub check_value { my ($class, $type, $key, $value, $storeid, $skipSchemaCheck) = @_; @@ -473,6 +810,46 @@ sub check_value { return $value; } +=pod + +=head3 parse_section_header + +B Can be I in the I. + + ($type, $sectionId, $errmsg, $config) = PVE::Example::BasePlugin->parse_section_header($line) + ($type, $sectionId, $errmsg, $config) = $class->parse_section_header($line) + +Parses the header of a section and returns an array containing the section's +C, ID and optionally an error message as well as additional config +attributes. + +Can be overriden on the I in order to provide custom logic for +handling the header, e.g. if the section IDs need to be parsed or validated in +a certain way. + +Note that the section B initially be parsed with the regex used by the +original method when overriding in order to guarantee compatibility. +For example: + + sub parse_section_header { + my ($class, $line) = @_; + + if ($line =~ m/^(\S):\s*(\S+)\s*$/) { + my ($type, $sectionId) = ($1, $2); + + my $errmsg = undef; + eval { check_section_id_is_valid($sectionId); }; + $errmsg = $@ if $@; + + my $config = parse_extra_stuff_from_section_id($sectionId); + + return ($type, $sectionId, $errmsg, $config); + } + return undef; + } + +=cut + sub parse_section_header { my ($class, $line) = @_; @@ -485,12 +862,40 @@ sub parse_section_header { return undef; } +=pod + +=head3 format_section_header + +B Can be overridden in the I. + + $header = PVE::Example::BasePlugin->format_section_header($type, $sectionId, $scfg, $done_hash) + $header = $class->format_section_header($type, $sectionId, $scfg, $done_hash) + +Formats the header of a section. Simply C<"$type: $sectionId\n"> by default. + +Note that when overriding this, the header B end with a newline (C<\n>). +One also might want to add a matching override for C. + +=cut + sub format_section_header { my ($class, $type, $sectionId, $scfg, $done_hash) = @_; return "$type: $sectionId\n"; } +=pod + +=head3 get_property_schema + + $schema = PVE::Example::BasePlugin->get_property_schema($type, $key) + $schema = $class->get_property_schema($type, $key) + +Returns the schema of a I of a I that is denoted via +its C<$type>. + +=cut + sub get_property_schema { my ($class, $type, $key) = @_; @@ -506,6 +911,106 @@ sub get_property_schema { return $schema; } +=pod + +=head3 parse_config + + $config = PVE::Example::BasePlugin->parse_config($filename, $raw, $allow_unknown) + $config = $class->parse_config($filename, $raw, $allow_unknown) + +Parses the contents of a C file and returns a complex nested +hash which not only contains the parsed data, but additional information that +one may or may not find useful. More below. + +=over + +=item C<$filename> + +The name of the file whose content is stored in C<$raw>. + +=item C<$raw> + +The raw content of C<$filename>. + +=item C<$allow_unknown> + +Whether to allow parsing unknown I. + +=back + +The returned hash is structured as follows: + + { + ids => { + foo => { + key => value, + ... + }, + bar => { + key => value, + ... + }, + }, + order => { + foo => 1, + bar => 2, + }, + digest => "5f5513f8822fdbe5145af33b64d8d970dcf95c6e", + errors => ( + { + context => ..., + section => "section ID", + key => "some_key", + err => "error message", + }, + ... + ), + } + +=over + +=item C + +Each section's parsed configuration values, or more precisely, the I
and their associated configuration options as returned by +C. + +=item C + +The order in which the sections in C were parsed. + +=item C + +A SHA1 hex digest of the contents in C<$raw>. + +=item C (optional) + +An optional list of error hashes, where each hash contains the following keys: + +=over 2 + +=item C + +In which file and in which line the error was encountered. + +=item C
+ +In which section the error was encountered. + +=item C + +Which I the error corresponds to. + +=item C + +The error. + +=back + +=back + +=cut + sub parse_config { my ($class, $filename, $raw, $allow_unknown) = @_; @@ -642,6 +1147,23 @@ sub parse_config { return $cfg; } +=pod + +=head3 check_config + + $settings = PVE::Example::BasePlugin->check_config($sectionId, $config, $create, $skipSchemaCheck) + $settings = $class->check_config($sectionId, $config, $create, $skipSchemaCheck) + +Does not just check whether a section's configuration is valid, despite its +name, but also calls C (among other things) internally. + +Returns a hash which contains all I for the given C<$sectionId>. +In other words, all configured key-value pairs for the provided section. + +It's best to not override this. + +=cut + sub check_config { my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_; @@ -700,6 +1222,52 @@ my $format_config_line = sub { } }; +=pod + +=head3 write_config + + $output = PVE::Example::BasePlugin->write_config($filename, $cfg, $allow_unknown) + $output = $class->write_config($filename, $cfg, $allow_unknown) + +Generates the output that should be written to the C file. + +=over + +=item C<$filename> (unused) + +The name of the file to which the generated output will be written to. +This parameter is currently unused and has no effect. + +=item C<$cfg> + +The hash that represents the entire configuration that should be written. +This hash is expected to have the following format: + + { + ids => { + foo => { + key => value, + ... + }, + bar => { + key => value, + ... + }, + }, + order => { + foo => 1, + bar => 2, + }, + } + +=item C<$allow_unknown> + +Whether to allow writing sections with an unknown C. + +=back + +=cut + sub write_config { my ($class, $filename, $cfg, $allow_unknown) = @_; @@ -798,6 +1366,45 @@ sub assert_if_modified { PVE::Tools::assert_if_modified($cfg->{digest}, $digest); } +=pod + +=head3 delete_from_config + + $config = PVE::Example::BasePlugin->delete_from_config($config, $option_schema, $new_options, $to_delete) + $config = $class->delete_from_config($config, $option_schema, $new_options, $to_delete) + +Convenience method to delete key from a hash of configured I which +performs necessary checks beforehand. + +Note: The passed C<$config> is modified in place and also returned. + +=over + +=item C<$config> + +The section's configuration that the given I in C<$to_delete> should +be deleted from. + +=item C<$option_schema> + +The schema of the I associated with C<$config>. See the C +method. + +=item C<$new_options> + +The I which are to be added to C<$config>. Note that this method +doesn't add any I itself; this is to prohibit simultaneously +setting and deleting the same I. + +=item C<$to_delete> + +A reference to an array containing the names of the I to delete +from C<$config>. + +=back + +=cut + sub delete_from_config { my ($config, $option_schema, $new_options, $to_delete) = @_;