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.carr...@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 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel