On Thu, Jan 30, 2025 at 03:51:20PM +0100, Max Carrara wrote:
> This commit changes how plugins, both inbuilt and custom, are loaded
> and registered by the `PVE::Storage` module from the ground up.
> 
> This is done for mainly two reasons:
> 
>  1. Unifying the plugin loading mechanism so that inbuilt and custom
>     plugins are subjected to (almost) the same checks.
> 
>  2. Making it possible to easily extend the loading / registering
>     mechanism for additional compile-time (BEGIN phase [1]) checks.
> 
> The new plugin registration mechanism essentially consists of four
> stages:
> 
>  1. Load plugins into the Perl interpreter
>  2. Perform pre-registration checks
>  3. Registration
>  4. Perform post-registration checks
> 
> Each stage will perform its specific actions on inbuilt plugins first
> and on custom plugins afterwards.
> 
> In general, should a stage fail for an inbuilt plugin, an exception is
> thrown. For custom plugins a warning is emitted instead and the plugin
> won't be considered in the subsequent stage(s).
> 
> The new registration mechanism is equivalent to the old one, with a
> few minor, but notable exceptions:
> 
>  1. Stage 1. will also verify whether each plugin (inbuilt or custom)
>     is installed inside `/usr/share/perl5/` and not anywhere else in
>     order to guarantee that there are no conflicts while loading.

And also breaks testing changes with `perl -I`/PERLLIB env vars.

> 
>     Example: A particularly impertinent custom plugin *may* install
>     itself to another path in `@INC`, such as
>     `/usr/local/lib/x86_64-linux-gnu/perl/5.36.0` e.g., and mask
>     another inbuilt or custom plugin by having the exact same
>     namespace.
> 
>     While unlikely, this is now guarded against.
> 
>  2. The new registration machinery also checks whether inbuilt
>     plugins derive from `PVE::Storage::Plugin`.

That's not new?

> 
>  3. Inbuilt plugins are no longer imported using `use`, the same logic
>     using `require` is now used for both inbuilt and custom plugins.

As mentioned in the cover letter, this seems to trip up your point 1.

> 
>  4. The registration now happens inside the `import()` subroutine,
>     which makes it more predictable when plugins are actually loaded.
> 
>     This means that plugins from here on out are only registered when
>     running `use PVE::Storage;` and not when using `require
>     PVE::Storage;`. The difference here is somewhat subtle; to
>     elaborate, the statement `use PVE::Storage;` is equivalent to the
>     following [2]:
> 
>         BEGIN {
>             require "PVE/Storage.pm";
>             'PVE::Storage'->import();
>         }
> 
>     Because `PVE::Storage` isn't imported using a plain `require`
>     anywhere in our code, this change is safe to make.
> 
> [1]: 
> https://perldoc.perl.org/perlmod#BEGIN%2C-UNITCHECK%2C-CHECK%2C-INIT-and-END
> [2]: https://perldoc.perl.org/functions/use
> 
> Signed-off-by: Max Carrara <m.carr...@proxmox.com>
> ---
>  src/PVE/Storage.pm | 559 +++++++++++++++++++++++++++++++++++++++------
>  1 file changed, 489 insertions(+), 70 deletions(-)
> 
> diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
> index df4d62f..0c8ba64 100755
> --- a/src/PVE/Storage.pm
> +++ b/src/PVE/Storage.pm
> @@ -10,6 +10,8 @@ use IO::Socket::IP;
>  use IPC::Open3;
>  use File::Basename;
>  use File::Path;
> +use File::Spec;
> +use Carp;
>  use Cwd 'abs_path';
>  use Socket;
>  use Time::Local qw(timelocal);
> @@ -27,82 +29,12 @@ use PVE::RESTEnvironment qw(log_warn);
>  use PVE::Storage::Version;
>  
>  use PVE::Storage::Plugin;
> -use PVE::Storage::DirPlugin;
> -use PVE::Storage::LVMPlugin;
> -use PVE::Storage::LvmThinPlugin;
> -use PVE::Storage::NFSPlugin;
> -use PVE::Storage::CIFSPlugin;
> -use PVE::Storage::ISCSIPlugin;
> -use PVE::Storage::RBDPlugin;
> -use PVE::Storage::CephFSPlugin;
> -use PVE::Storage::ISCSIDirectPlugin;
> -use PVE::Storage::GlusterfsPlugin;
> -use PVE::Storage::ZFSPoolPlugin;
> -use PVE::Storage::ZFSPlugin;
> -use PVE::Storage::PBSPlugin;
> -use PVE::Storage::BTRFSPlugin;
> -use PVE::Storage::ESXiPlugin;
>  
>  use constant APIVER => PVE::Storage::Version::APIVER;
>  use constant APIAGE => PVE::Storage::Version::APIAGE;
>  
>  our $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 
> 'vmdk+size', 'zfs', 'btrfs'];
>  
> -# load standard plugins
> -PVE::Storage::DirPlugin->register();
> -PVE::Storage::LVMPlugin->register();
> -PVE::Storage::LvmThinPlugin->register();
> -PVE::Storage::NFSPlugin->register();
> -PVE::Storage::CIFSPlugin->register();
> -PVE::Storage::ISCSIPlugin->register();
> -PVE::Storage::RBDPlugin->register();
> -PVE::Storage::CephFSPlugin->register();
> -PVE::Storage::ISCSIDirectPlugin->register();
> -PVE::Storage::GlusterfsPlugin->register();
> -PVE::Storage::ZFSPoolPlugin->register();
> -PVE::Storage::ZFSPlugin->register();
> -PVE::Storage::PBSPlugin->register();
> -PVE::Storage::BTRFSPlugin->register();
> -PVE::Storage::ESXiPlugin->register();
> -
> -# load third-party plugins
> -if ( -d '/usr/share/perl5/PVE/Storage/Custom' ) {
> -    dir_glob_foreach('/usr/share/perl5/PVE/Storage/Custom', '.*\.pm$', sub {
> -     my ($file) = @_;
> -     my $modname = 'PVE::Storage::Custom::' . $file;
> -     $modname =~ s!\.pm$!!;
> -     $file = 'PVE/Storage/Custom/' . $file;
> -
> -     eval {
> -         require $file;
> -
> -         # Check perl interface:
> -         die "not derived from PVE::Storage::Plugin\n" if 
> !$modname->isa('PVE::Storage::Plugin');
> -         die "does not provide an api() method\n" if !$modname->can('api');
> -         # Check storage API version and that file is really storage plugin.
> -         my $version = $modname->api();
> -         die "implements an API version newer than current ($version > " . 
> APIVER . ")\n"
> -             if $version > APIVER;
> -         my $min_version = (APIVER - APIAGE);
> -         die "API version too old, please update the plugin ($version < 
> $min_version)\n"
> -             if $version < $min_version;
> -         # all OK, do import and register (i.e., "use")
> -         import $file;
> -         $modname->register();
> -
> -         # If we got this far and the API version is not the same, make some 
> noise:
> -         warn "Plugin \"$modname\" is implementing an older storage API, an 
> upgrade is recommended\n"
> -             if $version != APIVER;
> -     };
> -     if ($@) {
> -         warn "Error loading storage plugin \"$modname\": $@";
> -     }
> -    });
> -}

^ The above was so much shorter... (and worked :P)

> -
> -# initialize all plugins
> -PVE::Storage::Plugin->init();
> -
>  # the following REs indicate the number or capture groups via the trailing 
> digit
>  # CAUTION don't forget to update the digits accordingly after messing with 
> the capture groups
>  
> @@ -124,6 +56,493 @@ our $OVA_CONTENT_RE_1 = 
> qr/${SAFE_CHAR_WITH_WHITESPACE_CLASS_RE}+\.(qcow2|raw|vm
>  # FIXME remove with PVE 9.0, add versioned breaks for pve-manager
>  our $vztmpl_extension_re = $VZTMPL_EXT_RE_1;
>  
> +# PVE::Storage plugin registration machinery
> +
> +my $plugin_register_state = {
> +    index => {},

^ A comment explaining the contents there might be useful.

> +    plugins_initialised => 0,
> +};
> +
> +=head3 get_standard_plugin_names()
> +
> +Returns an array or a reference to an array of the module names of our 
> inbuilt
> +storage plugins, depending on the context of the caller.
> +
> +    # both valid
> +    my $standard_plugins = get_standard_plugin_names();
> +    my @standard_plugins = get_standard_plugin_names();

More POD clutter for something unnecessary 😬.

> +
> +=cut
> +
> +my sub get_standard_plugin_names : prototype() {

This should not be a sub.

> +    my @standard_plugins = qw(
> +     PVE::Storage::DirPlugin
> +     PVE::Storage::LVMPlugin
> +     PVE::Storage::LvmThinPlugin
> +     PVE::Storage::NFSPlugin
> +     PVE::Storage::CIFSPlugin
> +     PVE::Storage::ISCSIPlugin
> +     PVE::Storage::RBDPlugin
> +     PVE::Storage::CephFSPlugin
> +     PVE::Storage::ISCSIDirectPlugin
> +     PVE::Storage::GlusterfsPlugin
> +     PVE::Storage::ZFSPoolPlugin
> +     PVE::Storage::ZFSPlugin
> +     PVE::Storage::PBSPlugin
> +     PVE::Storage::BTRFSPlugin
> +     PVE::Storage::ESXiPlugin
> +    );
> +
> +    return @standard_plugins if wantarray;
> +    return \@standard_plugins;
> +}
> +
> +=head3 get_custom_plugin_names()
> +
> +Returns an array or a reference to an array of the module names of custom
> +storage plugins inside C</usr/share/perl5/PVE/Storage/Custom>, depending on 
> the
> +context of the caller.
> +
> +    # both valid
> +    my $custom_plugins = get_custom_plugin_names();
> +    my @custom_plugins = get_custom_plugin_names();

😬

and still not convinced private oneshot subs should use POD.

> +
> +=cut
> +
> +my sub get_custom_plugin_names : prototype() {
> +    my @custom_plugins = ();
> +
> +    if ( -d '/usr/share/perl5/PVE/Storage/Custom' ) {
> +     dir_glob_foreach('/usr/share/perl5/PVE/Storage/Custom', '.*\.pm$', sub {
> +         my ($file) = @_;
> +
> +         my $modname = 'PVE::Storage::Custom::' . $file;
> +         $modname =~ s!\.pm$!!;
> +
> +         push(@custom_plugins, $modname);
> +     });
> +    }
> +
> +    return @custom_plugins if wantarray;
> +    return \@custom_plugins;
> +}
> +
> +=head3 get_indexed_plugins(%params)
> +
> +Returns the names of the currently indexed storage plugins, optionally 
> allowing
> +certain filters flags to be passed. Returns an array or a reference to an 
> array
> +depending on the context of the caller.
> +
> +    my @all_plugins = get_indexed_plugins();
> +
> +    my $standard_plugins = get_indexed_plugins(is_custom => 0);
> +    my @custom_plugins = get_indexed_plugins(is_custom => 1);
> +
> +    my @registered_custom_plugins = get_indexed_plugins(is_custom => 1, 
> is_registered => 1);
> +
> +The following key-value pairs may optionally be passed and will cause this
> +subroutine to filter out any plugin names for which they don't apply:
> +
> +=over
> +
> +=item * C<< is_custom => [0|1] >>
> +
> +Whether to only return names of custom plugin names or inbuilt ones.
> +
> +=item * C<< is_checked => [0|1]>>
> +
> +Whether to only return plugins that passed or haven't passed the
> +pre-registration check.
> +
> +=item * C<< is_registered => [0|1] >>
> +
> +Whether to only return plugins that were or weren't registered.
> +
> +=back
> +
> +=cut
> +
> +my sub get_indexed_plugins : prototype(;%) {
> +    my (%params) = @_;
> +
> +    my $plugin_index = $plugin_register_state->{index};
> +
> +    my $test_index_for_flag = sub {
> +     my ($plugin_name, $flag) = @_;
> +
> +     return 0 if !defined($plugin_index->{$_});
> +     return 0 if !defined($plugin_index->{$_}->{$flag});
> +
> +     return $plugin_index->{$_}->{$flag} == $params{$flag};
> +    };
> +
> +    my @plugins = keys $plugin_index->%*;
> +
> +    @plugins = grep { $test_index_for_flag->($_, 'is_custom') } @plugins
> +     if defined($params{is_custom});

^ The helper-closure could do the entire `grep` statement to make this
just be

    $grep_for_flags->($flag) for (qw(is_checked ...));

at which point we could drop the closure and just do

    for my $flag (qw(is_custom is_checked is_registered) {
        # contents of helper sub
    }

IMO more readable.

> +
> +    @plugins = grep { $test_index_for_flag->($_, 'is_checked') } @plugins
> +     if defined($params{is_checked});
> +
> +    @plugins = grep { $test_index_for_flag->($_, 'is_registered') } @plugins
> +     if defined($params{is_registered});
> +
> +    return @plugins if wantarray;
> +    return @plugins;

^ no

> +}
> +
> +=head3 do_try_load_plugin(%params)
> +
> +Actual loading mechanism used inside C<L<< 
> try_load_plugins()|/"try_load_plugins()" >>>.
> +Attempts to load the plugin provided via the C<name> key. Optionally, C<< 
> is_custom => 1 >>
> +may be passed if a custom plugin is being loaded.
> +
> +    do_try_load_plugin(name => 'PVE::Storage::DirPlugin');
> +
> +    do_try_load_plugin(name => 'PVE::Storage::Custom::FooPlugin', is_custom 
> => 1);
> +
> +The given plugin is loaded dynamically via Perl's C<require> and then added 
> to
> +the plugin index for further operations, such as pre- and post-registration
> +checks as well as the actual registration itself.
> +
> +This subroutine performs additional sanity checks and will raise an 
> exception if
> +the plugin doesn't exist in C</usr/share/perl5/> or if the plugin was 
> already loaded.
> +
> +=cut
> +
> +my sub do_try_load_plugin : prototype(%) {
> +    my (%params) = @_;
> +
> +    my $plugin_name = $params{name};
> +    my $is_custom = $params{is_custom};
> +
> +    croak "no plugin name provided - must be fully qualified namespace, e.g. 
> Foo::Bar::Baz"
> +     if !defined($plugin_name);
> +
> +    my $base_path = '/usr/share/perl5/';
> +
> +    my $plugin_filepath = File::Spec->catdir(split('::', $plugin_name)) . 
> '.pm';
> +    my ($_volume, $plugin_dir, $plugin_filename) = 
> File::Spec->splitpath($plugin_filepath);
> +
> +    my $plugin_filepath_abs = File::Spec->catfile($base_path, $plugin_dir, 
> $plugin_filename);
> +
> +    croak "plugin '$plugin_name' does not exist at path 
> '$plugin_filepath_abs'"
> +     if (! -e $plugin_filepath_abs);
> +
> +    eval {
> +     require $plugin_filepath;
> +    };
> +    croak "Failed to load plugin '$plugin_name' - $@" if $@;
> +
> +    my $plugin_index = $plugin_register_state->{index};
> +
> +    confess "Plugin '$plugin_name' is already loaded" if 
> exists($plugin_index->{$plugin_name});
> +
> +    $plugin_index->{$plugin_name} = {
> +     absolute_path => $plugin_filepath_abs,
> +     is_custom => $is_custom || 0,
> +     is_checked => 0,
> +     is_registered => 0,
> +    };
> +
> +    return;
> +}
> +
> +=head3 try_load_plugins()
> +
> +Attempts to load all available plugins, with inbuilt ("standard") plugins 
> being
> +loaded first and custom plugins loaded afterwards.
> +
> +All standard plugins must successfully load, otherwise an exception is 
> thrown.
> +If a custom plugin fails to load, a warning is emitted instead.
> +
> +=cut
> +
> +my sub try_load_plugins : prototype() {
> +    my @errs = ();
> +
> +    for my $plugin (get_standard_plugin_names()) {
> +     eval {
> +         do_try_load_plugin(name => $plugin);
> +     };
> +
> +     push(@errs, $@) if $@;
> +    }
> +
> +    confess("Encountered unexpected error(s) while loading plugins:\n", 
> join("\n", @errs), "\n\n... ")
> +     if scalar(@errs);
> +
> +    for my $plugin (get_custom_plugin_names()) {
> +     eval {
> +         do_try_load_plugin(name => $plugin, is_custom => 1);
> +     };
> +
> +     warn "Error loading storage plugin \"$plugin\" - $@" if $@;
> +    }
> +
> +    return;
> +}
> +
> +=head3 do_pre_register_check_plugin(%params)
> +
> +Used within C<L<< 
> pre_register_check_plugins()|/"pre_register_check_plugins()" >>>
> +to perform the actual pre-registration checks for the plugin given by the 
> C<name>
> +key.
> +
> +    do_pre_register_check_plugin(name => 'PVE::Storage::DirPlugin');
> +
> +This checks whether the plugin is derived from C<L<PVE::Storage::Plugin>>. 
> If the
> +plugin is a custom plugin, its API conformance is also verified:
> +
> +=over
> +
> +=item * The plugin must provide an C<api()> method returning its API version
> +
> +=item * Its API version must not be newer than the current storage API 
> version
> +
> +=item * Its API version must not be older than the storage API age allows
> +
> +=back
> +
> +=cut
> +
> +my sub do_pre_register_check_plugin : prototype(%) {
> +    my (%params) = @_;
> +
> +    my $plugin_name = $params{name};
> +
> +    my $plugin_index = $plugin_register_state->{index};
> +
> +    croak "Plugin '$plugin_name' does not exist in index"
> +     if !defined($plugin_index->{$plugin_name});
> +
> +    my $is_custom = $plugin_index->{$plugin_name}->{is_custom};
> +
> +    my $check_derives_base = sub {
> +     # so that we may call methods on $plugin_name
> +     no strict 'refs'; ## no critic
> +
> +     die "not derived from PVE::Storage::Plugin\n"
> +         if !$plugin_name->isa('PVE::Storage::Plugin');
> +    };
> +
> +    my $check_plugin_api = sub {
> +     # so that we may call methods on $plugin_name
> +     no strict 'refs'; ## no critic
> +
> +     die "does not provide an 'api()' method\n"
> +         if !$plugin_name->can('api');
> +
> +     my $version = $plugin_name->api();
> +     my $min_version = (APIVER - APIAGE);
> +
> +     die "implements an API version newer than current ($version > 
> @{[APIVER]})\n"
> +         if $version > APIVER;
> +
> +     die "API version too old, please update the plugin ($version < 
> $min_version)\n"
> +         if $version < $min_version;
> +    };
> +
> +    eval {
> +     $check_derives_base->();
> +     $check_plugin_api->() if $is_custom;
> +    };
> +
> +    croak "Pre-registration check failed for plugin '$plugin_name' - $@" if 
> $@;
> +
> +    $plugin_index->{$plugin_name}->{is_checked} = 1;
> +
> +    return;
> +}
> +
> +=head3 pre_register_check_plugins()
> +
> +Performs various pre-registration checks for all plugins.
> +
> +Standard plugins must always pass all checks. If a custom plugin fails a 
> check,
> +a warning is emitted instead and it will not be registered later on.
> +
> +For more details on which checks are performed, see
> +C<L<< 
> do_pre_register_check_plugin()|/"do_pre_register_check_plugin(%params)" >>>.
> +
> +=cut
> +
> +my sub pre_register_check_plugins : prototype() {
> +    my @errs = ();
> +
> +    my @standard_plugins = get_indexed_plugins(is_custom => 0);
> +    for my $plugin (@standard_plugins) {
> +     eval {
> +         do_pre_register_check_plugin(name => $plugin);
> +     };
> +
> +     push(@errs, $@) if $@;
> +    }
> +
> +    if (scalar(@errs)) {
> +     my $err_msg = "Encountered unexpected error while performing"
> +         . " pre-registration check for inbuilt plugins:\n";
> +
> +     confess($err_msg, join("\n", @errs), "\n\n... ");
> +    }
> +
> +    my @custom_plugins = get_indexed_plugins(is_custom => 1);
> +    for my $plugin (@custom_plugins) {
> +     eval {
> +         do_pre_register_check_plugin(name => $plugin);
> +     };
> +
> +     if ($@) {
> +         warn "Encountered unexpected error while performing 
> pre-registration"
> +             . " check for custom plugin \"$plugin\":\n$@\n";
> +         warn "Plugin will not be registered.\n";
> +     }
> +    }
> +
> +    return;
> +}
> +
> +=head3 try_register_plugins()
> +
> +Attempts to register all plugins of the plugin index for which the
> +L<pre-registration check|/"pre_register_check_plugins()"> was successful, 
> with
> +standard plugins being registered first and custom plugins afterwards.
> +
> +In detail, for each plugin, the C<import()> subroutine is first run, followed
> +by C<L<< register()|PVE::SectionConfig/register >>>.
> +
> +For all standard plugins both invocations must succeed, otherwise an 
> exception
> +is thrown. If a call to either subroutine fails for a custom plugin, a 
> warning
> +is emitted instead and the custom plugin remains unregistered.
> +
> +=cut
> +
> +my sub try_register_plugins : prototype() {
> +    my @errs = ();
> +
> +    my $plugin_index = $plugin_register_state->{index};
> +
> +    my $try_import = sub {
> +     my ($plugin_name) = @_;
> +
> +     # so that we may call methods on $plugin_name
> +     no strict 'refs'; ## no critic
> +     $plugin_name->import();

This needs to happen in `try_load_plugins` or
`pre_register_check_plugins`, otherwise `->isa()` won't work.

> +    };
> +
> +    my $try_register = sub {
> +     my ($plugin_name) = @_;
> +
> +     # so that we may call methods on $plugin_name
> +     no strict 'refs'; ## no critic
> +     $plugin_name->register();
> +
> +     $plugin_index->{$plugin_name}->{is_registered} = 1;
> +    };
> +
> +    my @standard_plugins = get_indexed_plugins(is_custom => 0, is_checked => 
> 1);
> +    for my $plugin (@standard_plugins) {
> +     eval {
> +         $try_import->($plugin);
> +     };
> +
> +     if ($@) {
> +         push(@errs, "Encountered unexpected error while running 
> '$plugin\->import()' - $@");
> +         next;
> +     }
> +
> +     eval {
> +         $try_register->($plugin);
> +     };
> +
> +     if ($@) {
> +         push(@errs, "Encountered unexpected error while running 
> '$plugin\->register()' - $@");
> +         next;
> +     }
> +    }
> +
> +    confess join("\n\n", @errs, "\n\n... ") if scalar(@errs);
> +
> +    my @custom_plugins = get_indexed_plugins(is_custom => 1, is_checked => 
> 1);
> +    for my $plugin (@custom_plugins) {
> +     eval {
> +         $try_import->($plugin);
> +     };
> +
> +     if ($@) {
> +         carp "Failed to run 'import' on custom plugin '$plugin' - $@";
> +         carp "Please check if the plugin is installed correctly.";
> +         carp "Continuing to register remaining custom plugins (if any).";
> +
> +         next;
> +     }
> +
> +     eval {
> +         $try_register->($plugin);
> +     };
> +
> +     if ($@) {
> +         carp "Failed to register custom plugin '$plugin' - $@";
> +         carp "Please verify whether the plugin is installed correctly.";
> +         carp "Continuing to register remaining custom plugins (if any).";
> +
> +         next;
> +     }
> +    }
> +
> +    return;
> +}
> +
> +=head3 post_register_check_plugins()
> +
> +Used to perform various checks on each plugin after the plugins' 
> registration.
> +
> +This subroutine currently only emits a warning if a custom plugin's API 
> version
> +is older than the current one.
> +
> +=cut
> +
> +my sub post_register_check_plugins : prototype() {
> +    my @custom_plugins = get_indexed_plugins(is_custom => 1, is_registered 
> => 1);
> +    for my $plugin (@custom_plugins) {
> +     # so that we may call methods on $plugin
> +     no strict 'refs'; ## no critic
> +
> +     my $version = $plugin->api();
> +     warn "Plugin \"$plugin\" is implementing an older storage API, an 
> upgrade is recommended\n"
> +         if $version != APIVER;
> +    }
> +
> +    return;
> +}
> +
> +# NOTE: This is *not* like Exporter's import (that you get with e.g. `use 
> parent qw(Exporter);`)
> +# which supports @EXPORT_OK etc.
> +#
> +# If there is ever a need to support exporting subroutines (via `Exporter`),
> +# see the following: 
> https://perldoc.perl.org/Exporter#Exporting-Without-Using-Exporter's-import-Method
> +# Beware that this requires you to remove any keys that ought not be passed
> +# to exporter from @_.
> +
> +sub import {
> +    my ($package, %params) = @_;
> +
> +    # this sub may be called more than once, so avoid registering plugins 
> again
> +    if ($plugin_register_state->{plugins_initialised} == 1) {
> +     return;
> +    }
> +
> +    try_load_plugins();
> +    pre_register_check_plugins();
> +    try_register_plugins();
> +    post_register_check_plugins();
> +
> +    PVE::Storage::Plugin->init();
> +    $plugin_register_state->{plugins_initialised} = 1;
> +
> +    return;
> +}
> +
>  #  PVE::Storage utility functions
>  
>  sub config {
> -- 
> 2.39.5


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to