[pve-devel] [PATCH v2 pve-common 1/4] section config: document package and its methods with POD

Max Carrara m.carrara at proxmox.com
Tue Jul 2 16:13:11 CEST 2024


Apart from the obvious benefits that documentation has, this also
allows LSPs to provide docstrings e.g. via 'textDocument/hover' [0].

Tested with Perl Navigator [1].

[0]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover
[1]: https://github.com/bscan/PerlNavigator

Signed-off-by: Max Carrara <m.carrara at proxmox.com>
---
Changes v1 --> v2:
  * basically all of @Fabian's feedback [0] was taken into account,
    which is way too much to list here, so please see the feedback
    itself
  * use more links where appropriate, e.g. to refer to methods
    associated with a term
  * provide more example code (subroutines `private` and `decode_value`)

[0]: https://lists.proxmox.com/pipermail/pve-devel/2024-June/064165.html

 src/PVE/SectionConfig.pm | 953 ++++++++++++++++++++++++++++++++++++---
 1 file changed, 888 insertions(+), 65 deletions(-)

diff --git a/src/PVE/SectionConfig.pm b/src/PVE/SectionConfig.pm
index a18e9d8..f768eb2 100644
--- a/src/PVE/SectionConfig.pm
+++ b/src/PVE/SectionConfig.pm
@@ -10,65 +10,103 @@ 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<Section Config>.
+
+For each C<SectionConfig>-based config file, a C<PVE::JSONSchema> is derived
+automatically. This schema can be used to implement CRUD operations for
+the config data.
+
+The location of a config file is chosen by the author of the code that uses
+C<SectionConfig> and is not something this module is concerned with.
+
+=head1 USAGE
+
+The intended structure is to have a single I<base plugin> that uses the
+C<SectionConfig> module as a base module. Furthermore, it should provide
+meaningful defaults in its C<$defaultData>, such as a default list of core
+C<PVE::JSONSchema> I<properties>. The I<base plugin> is thus very similar to an
+I<abstract class>.
+
+Each I<child plugin> is then defined in its own package that should inherit
+from the I<base plugin> and defines which I<properties> it itself provides and
+uses, as well as which I<properties> it uses from the I<base plugin>.
+
+The methods that need to be implemented are annotated in the L</METHODS> section
+below.
+
+              ┌─────────────────┐
+              │  SectionConfig  │
+              └────────┬────────┘
+                       │
+                       │
+                       │
+              ┌────────▼────────┐
+              │    BasePlugin   │
+              └────────┬────────┘
+                       │
+             ┌─────────┴─────────┐
+             │                   │
+    ┌────────▼────────┐ ┌────────▼────────┐
+    │ConcretePluginFoo│ │ConcretePluginBar│
+    └─────────────────┘ └─────────────────┘
+
+=head2 REGISTERING PLUGINS
+
+In order to actually be able to use plugins, they must first be
+I<L<registered|/register>> and then I<L<initialized|/init>> via the
+I<"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<properties> are exposed.
+
+=head3 unified mode (default)
+
+In this mode there is only a global list of I<properties> 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<L</init>> with an additional parameter:
+
+    PVE::Example::BasePlugin->init(property_isolation => 1);
+
+With this mode each I<child plugin> gets its own isolated list of I<properties>,
+or in other words, a fully isolated schema namespace. Normally one wants to use
+C<oneOf> schemas when enabling isolation.
+
+Note that in this mode it's only necessary to specify a I<property> in the
+return value of the C<options> method when it's either C<fixed> or stems from
+the global list of I<properties>.
+
+All locally defined I<properties> of a I<child plugin> are automatically added
+to its schema.
+
+=head2 METHODS
+
+=cut
 
 my $defaultData = {
     options => {},
@@ -77,11 +115,127 @@ my $defaultData = {
     propertyList => {},
 };
 
+=pod
+
+=head3 private
+
+B<REQUIRED:> Must be implemented in the I<base plugin>.
+
+    $data = PVE::Example::Plugin->private()
+    $data = $class->private()
+
+Returns the entire internal state of C<SectionConfig>, where all plugins as well
+as their C<L</options>> and more are being tracked.
+
+More precisely, this method returns a hash with the following structure:
+
+    {
+	propertyList => {
+	    'some-optional-property' => {
+		type => 'string',
+		optional => 1,
+		description => 'example property',
+	    },
+	    some-property => {
+		description => 'another example property',
+		type => 'boolean'
+	    },
+	},
+	options => {
+	    foo => {
+		'some-optional-property' => { optional => 1 },
+		...
+	    },
+	    ...
+	},
+	plugins => {
+	    foo => 'PVE::Example::FooPlugin',  # reference to package of child plugin
+	    ...
+	},
+	plugindata => {
+	    foo => { ... },  # depends on the specific plugin architecture
+	},
+    }
+
+Where C<foo> is the C<L</type>> of the plugin. See C<L</options>> and
+C<L</plugindata>> for more information on their corresponding keys above.
+
+Most commonly this is used to define the default I<property list> of one's
+plugin architecture upfront, 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;
+    }
+
+Additional I<properties> defined in I<child plugins> are stored in the
+C<propertyList> key. See C<L</properties>>.
+
+=cut
+
 sub private {
     die "overwrite me";
     return $defaultData;
 }
 
+=pod
+
+=head3 register
+
+    PVE::Example::Plugin->register()
+
+Used to register I<child plugins>.
+
+More specifically, I<registering> a child plugin means that it is added to the
+list of known child plugins that is kept in the hash returned by C<L</private>>.
+Furthermore, the data returned by C<L</plugindata>> is also stored upon
+registration.
+
+This method must be called on each child plugin before I<L<initializing|/init>>
+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 +250,153 @@ sub register {
     $pdata->{plugins}->{$type} = $class;
 }
 
+=pod
+
+=head3 type
+
+B<REQUIRED:> Must be implemented in I<B<each>> I<child plugin>.
+
+    $type = PVE::Example::Plugin->type()
+    $type = $class->type()
+
+Returns the I<type> of a I<child plugin>, which is a I<unique> string. This is
+used to identify the I<child plugin>.
+
+Must be overridden on I<B<each>> I<child plugin>, for example:
+
+    sub type {
+	return "foo";
+    }
+
+=cut
+
 sub type {
     die "overwrite me";
 }
 
+=pod
+
+=head3 properties
+
+B<OPTIONAL:> Can be overridden in I<child plugins>.
+
+    $props = PVE::Example::Plugin->properties()
+    $props = $class->properties()
+
+Used to register additional I<properties> that belong to a I<child plugin>.
+See below for details on L<the different modes|/MODES>.
+
+This method doesn't need to be overridden if no new properties are necessary.
+
+    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'),
+	};
+    }
+
+In the default I<L<unified mode|/MODES>>, these properties are added to the
+global list of properties. This means they may also be used by other plugins,
+rather than just by itself. The same property must not be defined by other
+plugins.
+
+In I<L<isolated mode|/MODES>>, these properties are specific to the plugin
+itself and cannot be used by others. They are however automatically added to
+the plugin's schema and made C<optional> by default.
+
+See the C<L</options>> method for more information.
+
+=cut
+
 sub properties {
     return {};
 }
 
+=pod
+
+=head3 options
+
+B<OPTIONAL:> Can be overridden in I<child plugins>.
+
+    $opts = PVE::Example::Plugin->options()
+    $opts = $class->options()
+
+This method is used to specify which I<properties> are actually allowed for
+a given I<child plugin>. See below for details on L<the different modes|/MODES>.
+
+Additionally, this method also allows to declare whether a property is
+C<optional> or C<fixed>.
+
+    sub options {
+	return {
+	    'some-optional-property' => { optional => 1 },
+	    'a-fixed-property' => { fixed => 1 },
+	    'a-required-but-not-fixed-property' => {},
+	};
+    }
+
+C<optional> I<properties> are not required to be set.
+
+C<fixed> I<properties> may only be set on creation of the config entity.
+
+In I<L<unified mode|/MODES>> (default), it is necessary to explicitly specify
+which I<properties> are used in the method's return value. Because properties
+are registered globally in this mode, any properties may be specified,
+regardless of which plugin introduced them.
+
+In I<L<isolated mode|/MODES>>, the locally defined properties (those registered
+by overriding C<L</properties>>) are automatically added to the plugin's schema
+and made C<optional> by default. Should this not be desired, a property may
+still be explicitly defined, in order to make it required or C<fixed> instead.
+
+Properties in the global list of properties (see C<L</private>>) are not
+automatically added and must be explicitly defined instead.
+
+=cut
+
 sub options {
     return {};
 }
 
+=pod
+
+=head3 plugindata
+
+B<OPTIONAL:> Can be implemented in I<child plugins>.
+
+    $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<SectionConfig>.
+
+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<properties> (runs in isolated mode).
+
+=cut
+
 sub has_isolated_properties {
     my ($class) = @_;
 
@@ -168,6 +453,33 @@ my sub add_property {
     }
 };
 
+=pod
+
+=head3 createSchema
+
+    $schema = PVE::Example::Plugin->($skip_type, $base)
+    $schema = $class->($skip_type, $base)
+
+Returns the C<PVE::JSONSchema> used for I<creating> config entries of a
+I<child plugin>.
+
+This schema may then be used as desired, for example as the definition of
+parameters of an API handler (C<POST>).
+
+=over
+
+=item C<$skip_type> (optional)
+
+Can be set to C<1> to not add the C<type> property to the schema.
+
+=item C<$base> (optional)
+
+The schema of additional properties not derived from the plugin definition.
+
+=back
+
+=cut
+
 sub createSchema {
     my ($class, $skip_type, $base) = @_;
 
@@ -242,6 +554,36 @@ sub createSchema {
     };
 }
 
+=pod
+
+=head3 updateSchema
+
+    $updated_schema = PVE::Example::Plugin->($single_class, $base)
+    $updated_schema = $class->updateSchema($single_class, $base)
+
+Returns the C<PVE::JSONSchema> used for I<updating> config entries of a
+I<child plugin>.
+
+This schema may then be used as desired, for example as the definition of
+parameters of an API handler (C<PUT>).
+
+=over
+
+=item C<$single_class> (optional)
+
+Can be set to C<1> to only include properties which are defined in the returned
+hash of I<L</options>> of the plugin C<$class>.
+
+This parameter is only valid for child plugins, not the base plugin.
+
+=item C<$base> (optional)
+
+The schema of additional properties not derived from the plugin definition.
+
+=back
+
+=cut
+
 sub updateSchema {
     my ($class, $single_class, $base) = @_;
 
@@ -326,12 +668,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 C<SectionConfig> using all of the
+I<child plugins> that were I<L<registered|/register>> beforehand.
+
+Optionally, it is also possible to pass C<property_isolation =E<gt> 1> as
+to C<%param> in order to activate I<isolated mode>. See L</MODES> in the
+package-level documentation for more information.
+
+=cut
+
 sub init {
     my ($class, %param) = @_;
 
@@ -392,6 +744,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<child plugin> corresponding to the given I<L</type>> or dies if it
+cannot be found.
+
+=cut
+
 sub lookup {
     my ($class, $type) = @_;
 
@@ -405,6 +769,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<child plugin> C<type>s.
+
+=cut
+
 sub lookup_types {
     my ($class) = @_;
 
@@ -413,18 +788,159 @@ sub lookup_types {
     return [ sort keys %{$pdata->{plugins}} ];
 }
 
+=pod
+
+=head3 decode_value
+
+B<OPTIONAL:> Can be implemented in the I<base plugin>.
+
+    $decoded_value = PVE::Example::BasePlugin->decode_value($type, $key, $value)
+    $decoded_value = $class->($type, $key, $value)
+
+Called during C<check_config> in order to convert values that have been read
+from a C<SectionConfig> file which have been I<encoded> beforehand by
+C<encode_value>.
+
+Does nothing to C<$value> by default, but can be overridden in the I<base plugin>
+in order to implement custom conversion behavior.
+
+    sub decode_value {
+	my ($class, $type, $key, $value) = @_;
+
+	if ($key eq 'nodes') {
+	    my $res = {};
+
+	    for my $node (PVE::Tools::split_list($value)) {
+		if (PVE::JSONSchema::pve_verify_node_name($node)) {
+		    $res->{$node} = 1;
+		}
+	    }
+
+	    return $res;
+	}
+
+	return $value;
+    }
+
+=over
+
+=item C<$type>
+
+The C<L</type>> of plugin the C<$key> and C<$value> belong to.
+
+=item C<$key>
+
+The name of a I<L<property|/properties>> that has been set on a C<$type> of
+config section.
+
+=item C<$value>
+
+The raw value of the I<L<property|/properties>> denoted by C<$key> that was read
+from a section config file.
+
+=back
+
+=cut
+
 sub decode_value {
     my ($class, $type, $key, $value) = @_;
 
     return $value;
 }
 
+=pod
+
+=head3 encode_value
+
+B<OPTIONAL:> Can be implemented in the I<base plugin>.
+
+    $encoded_value = PVE::Example::BasePlugin->encode_value($type, $key, $value)
+    $encoded_value = $class->($type, $key, $value)
+
+Called during C<write_config> in order to convert values into a serializable
+format.
+
+Does nothing to C<$value> by default, but can be overridden in the I<base plugin>
+in order to implement custom conversion behavior. Usually one should also
+override C<decode_value> in a matching manner.
+
+    sub encode_value {
+	my ($class, $type, $key, $value) = @_;
+
+	if ($key eq 'nodes') {
+	    return join(',', keys(%$value));
+	}
+
+	return $value;
+    }
+
+=over
+
+=item C<$type>
+
+The C<L</type>> of plugin the C<$key> and C<$value> belong to.
+
+=item C<$key>
+
+The name of a I<L<property|/properties>> that has been set on a C<$type> of
+config section.
+
+=item C<$value>
+
+The value of the I<L<property|/properties>> denoted by C<$key> to be encoded so
+that it can be written to a section config file.
+
+=back
+
+=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 when parsing a section
+config file. Also performs a C<PVE::JSONSchema> check on the C<$value> of the
+I<property> given by C<$key> of the plugin C<$type>, unless C<$skipSchemaCheck>
+is truthy.
+
+=over
+
+=item C<$type>
+
+The C<L</type>> of plugin the C<$key> and C<$value> belong to.
+
+=item C<$key>
+
+The name of a I<L<property|/properties>> that has been set on a C<$type> of
+config section.
+
+=item C<$value>
+
+The value of the I<L<property|/properties>> denoted by C<$key> that was read
+from a section config file.
+
+=item C<$storeid>
+
+The identifier of a section, as returned by C<L</parse_section_header>>.
+
+=item C<$skipSchemaCheck>
+
+Whether to skip performing a C<PVE::JSONSchema> property check on the given
+C<$value>.
+
+=back
+
+=cut
+
 sub check_value {
     my ($class, $type, $key, $value, $storeid, $skipSchemaCheck) = @_;
 
@@ -473,6 +989,44 @@ sub check_value {
     return $value;
 }
 
+=pod
+
+=head3 parse_section_header
+
+B<OPTIONAL:> Can be overridden in the I<base plugin>.
+
+    ($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<type>, ID and optionally an error message as well as additional config
+attributes.
+
+Can be overriden on the I<base plugin> 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.
+
+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 +1039,39 @@ sub parse_section_header {
     return undef;
 }
 
+=pod
+
+=head3 format_section_header
+
+B<OPTIONAL:> Can be overridden in the I<base plugin>.
+
+    $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<MUST> end with a newline (C<\n>).
+One also might want to add a matching override for C<parse_section_header>.
+
+=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 the L<property|/properties> C<$key> of the plugin for C<$type>.
+
+=cut
+
 sub get_property_schema {
     my ($class, $type, $key) = @_;
 
@@ -506,6 +1087,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 file as C<SectionConfig>, returning the parsed data
+annoted with additional information (see below).
+
+=over
+
+=item C<$filename>
+
+The name of the file whose content is stored in C<$raw>.
+
+Only used for error messages and warnings, so it may also be something else.
+
+=item C<$raw>
+
+The raw content of C<$filename>.
+
+=item C<$allow_unknown>
+
+Whether to allow parsing unknown I<types>.
+
+=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<ids>
+
+Each section's parsed data (via C<L</check_config>>), indexed by the section ID.
+
+=item C<order>
+
+The order in which the sections in C<ids> were found in the config file.
+
+=item C<digest>
+
+A SHA1 hex digest of the contents in C<$raw>. May for example be used to check
+whether the configuration has changed between parses.
+
+=item C<errors> (optional)
+
+An optional list of error hashes, where each hash contains the following keys:
+
+=over 2
+
+=item C<context>
+
+In which file and in which line the error was encountered.
+
+=item C<section>
+
+In which section the error was encountered.
+
+=item C<key>
+
+Which I<property> the error corresponds to.
+
+=item C<err>
+
+The error.
+
+=back
+
+=back
+
+=cut
+
 sub parse_config {
     my ($class, $filename, $raw, $allow_unknown) = @_;
 
@@ -642,6 +1323,58 @@ 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 checks values of I<L</properties>> with C<L</check_value>>
+before decoding them using C<L</decode_value>>.
+
+Returns a hash which contains all I<L</properties>> for the given C<$sectionId>.
+In other words, all configured key-value pairs for the provided section.
+
+=over
+
+=item C<$sectionId>
+
+The identifier of a section, as returned by C<L</parse_section_header>>.
+
+=item C<$config>
+
+The configuration of the section corresponding to C<$sectionId>.
+
+=item C<$create>
+
+If set to C<1>, checks whether a value has been set for all required
+I<L</properties>> of C<$config>
+
+=item C<$skipSchemaCheck>
+
+Whether to skip performing any C<PVE::JSONSchema> property checks.
+
+=back
+
+=head4 A Note on Extending and Overriding
+
+If additional checks are needed that cannot be expressed in the schema, this
+method may be extended or overridden I<with care>.
+
+When this method is I<overridden>, as in the original implementation is replaced
+completely, all values must still be checked via C<L</check_value>> and decoded
+with C<L</decode_value>>.
+
+When extending the method, as in calling C<$class-E<gt>SUPER::check_config>
+inside the redefined method, it is important to note that the contents of the
+hash returned by the C<SUPER> call differ from the contents of C<$config>. This
+means that a custom check performed I<before> the C<SUPER> call cannot
+necessarily be performed in the same way I<after> the C<SUPER> call.
+
+=cut
+
 sub check_config {
     my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
 
@@ -700,6 +1433,55 @@ 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<SectionConfig> 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,
+	},
+    }
+
+Any other top-level keys will be ignored, so it's okay to pass along the
+C<digest> key from C<L</parse_config>>, for example.
+
+=item C<$allow_unknown>
+
+Whether to allow writing sections with an unknown I<L</type>>.
+
+=back
+
+=cut
+
 sub write_config {
     my ($class, $filename, $cfg, $allow_unknown) = @_;
 
@@ -798,6 +1580,47 @@ sub assert_if_modified {
     PVE::Tools::assert_if_modified($cfg->{digest}, $digest);
 }
 
+=pod
+
+=head3 delete_from_config
+
+    $config = delete_from_config($config, $option_schema, $new_options, $to_delete)
+
+Convenience helper method used internally to delete keys from the single section
+config C<$config>.
+
+Specifically, the keys given by C<$to_delete> are deleted from C<$config> if
+they're not required or fixed, or set in the same go.
+
+Note: The passed C<$config> is modified in place and also returned.
+
+=over
+
+=item C<$config>
+
+The section's configuration that the given I<L</properties>> in C<$to_delete>
+should be deleted from.
+
+=item C<$option_schema>
+
+The schema of the I<L</properties>> associated with C<$config>. See the
+C<L</options>> method.
+
+=item C<$new_options>
+
+The I<L</properties>> which will be added to C<$config>. Note that this method
+doesn't add any I<L</properties>> itself; this is to prohibit simultaneously
+setting and deleting the same I<property>.
+
+=item C<$to_delete>
+
+A reference to an array containing the names of the I<properties> to delete
+from C<$config>.
+
+=back
+
+=cut
+
 sub delete_from_config {
     my ($config, $option_schema, $new_options, $to_delete) = @_;
 
-- 
2.39.2





More information about the pve-devel mailing list