From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id A52DA1FF0ED for ; Fri, 31 Jul 2026 17:37:33 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id D542321698; Fri, 31 Jul 2026 17:36:40 +0200 (CEST) From: Jakob Klocker To: pve-devel@lists.proxmox.com Subject: [PATCH pve-manager 6/9] cluster: backend: include storages with unloadable plugins Date: Fri, 31 Jul 2026 17:36:14 +0200 Message-ID: <20260731153617.358265-7-j.klocker@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260731153617.358265-1-j.klocker@proxmox.com> References: <20260731153617.358265-1-j.klocker@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 1 AWL -0.568 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 1.274 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Message-ID-Hash: D3E6JYVG37KVFAU5SC5YUB2QZS4YMM4D X-Message-ID-Hash: D3E6JYVG37KVFAU5SC5YUB2QZS4YMM4D X-MailFrom: jklocker@dev.proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Jakob Klocker X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Storages whose plugins failed to load (broken third party plugins, unknown plugins) were previously dropped from the cluster resource list, leaving them invisible in the tree. Switch to `config(1)` so storages with unregistered types are still returned and emit data stored in the cached failed-plugin metadata. Add `pluginstate` and `plugin-url` to storage resource entries (for both, loaded and failed plugins) so the GUI can show plugin state and link to the plugin's documentation. Signed-off-by: Jakob Klocker --- PVE/API2/Cluster.pm | 51 +++++++++++++++++++++++++++++++++++++++++++-- PVE/API2Tools.pm | 6 ++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm index 4e5efbfd..432ee64b 100644 --- a/PVE/API2/Cluster.pm +++ b/PVE/API2/Cluster.pm @@ -435,6 +435,18 @@ __PACKAGE__->register_method({ format => 'pve-storage-content-list', optional => 1, }, + 'pluginstate' => { + description => "State of the storage plugin (for type 'storage').", + type => 'string', + enum => ['up-to-date', 'outdated', 'load-error'], + optional => 1, + }, + 'plugin-url' => { + description => + "URL to the plugin's homepage or documentation (for type 'storage').", + type => 'string', + optional => 1, + }, shared => { description => "Determines whether the storage is shared", type => 'boolean', @@ -640,14 +652,49 @@ __PACKAGE__->register_method({ if (!$param->{type} || $param->{type} eq 'storage') { - my $cfg = PVE::Storage::config(); + my $cfg = PVE::Storage::config(1); my @sids = PVE::Storage::storage_ids($cfg); + my $failed = PVE::Storage::failed_third_party_plugins(); + my $failed_by_type = {}; + for my $modname (keys %$failed) { + my $f = $failed->{$modname}; + $failed_by_type->{ $f->{type} } = $f if defined $f->{type}; + } + foreach my $storeid (@sids) { next if !$rpcenv->check($authuser, "/storage/$storeid", ['Datastore.Audit'], 1); my $scfg = PVE::Storage::storage_config($cfg, $storeid); - # we create a entry for each node + + # if no plugin is registered for this type (failed to load or unknown), + # emit a minimal entry from cached metadata and skip the stats path + my $plugin = eval { PVE::Storage::Plugin->lookup($scfg->{type}) }; + if (!$plugin) { + my $failed_plugin = $failed_by_type->{ $scfg->{type} }; + my $content = $scfg->{content} // ''; + foreach my $node (@$nodelist) { + next if !PVE::Storage::storage_check_enabled($cfg, $storeid, $node, 1); + push @$res, + { + id => "storage/$node/$storeid", + storage => $storeid, + node => $node, + type => 'storage', + plugintype => $scfg->{type}, + status => 'unknown', + shared => $scfg->{shared} || 0, + content => $content, + pluginstate => 'load-error', + 'plugin-url' => $failed_plugin + ? $failed_plugin->{'plugin-url'} + : undef, + }; + } + next; + } + + # we create an entry for each node foreach my $node (@$nodelist) { next if !PVE::Storage::storage_check_enabled($cfg, $storeid, $node, 1); diff --git a/PVE/API2Tools.pm b/PVE/API2Tools.pm index bca21aa5..247b9c7a 100644 --- a/PVE/API2Tools.pm +++ b/PVE/API2Tools.pm @@ -15,6 +15,7 @@ use PVE::Exception qw(raise_param_exc); use PVE::INotify; use PVE::RPCEnvironment; use PVE::SafeSyslog; +use PVE::Storage; use PVE::Storage::Plugin; use PVE::Tools; @@ -122,6 +123,9 @@ sub extract_storage_stats { my ($storeid, $scfg, $node, $rrd) = @_; my $content = PVE::Storage::Plugin::content_hash_to_string($scfg->{content}); + my $plugin_url = + eval { PVE::Storage::Plugin->lookup($scfg->{type})->plugindata()->{'plugin-url'} }; + my $pluginstate = PVE::Storage::get_plugin_state($scfg->{type}); my $entry = { id => "storage/$node/$storeid", @@ -132,6 +136,8 @@ sub extract_storage_stats { status => 'unknown', shared => $scfg->{shared} || 0, content => $content, + pluginstate => $pluginstate, + 'plugin-url' => $plugin_url, }; my $key = get_rrd_key($rrd, "storage", "${node}/${storeid}"); -- 2.47.3