* [PATCH manager/storage 0/9] storage: plugins: display failed and outdated storage plugins in the GUI
@ 2026-07-31 15:36 Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 1/9] storage: drop unused variable in storage_info Jakob Klocker
` (8 more replies)
0 siblings, 9 replies; 10+ messages in thread
From: Jakob Klocker @ 2026-07-31 15:36 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
Currently a storage whose plugin fails to load - because it is a
third-party plugin that is outdated, broken, or not installed on a
node - is silently dropped from the cluster resource tree, giving the
user no indication why it's gone or how to fix it.
This series makes such storages visible and displays plugin
metadata to explain the situation.
The storage backend now records, per plugin, whether it is a built-in
or a third-party plugin, its API version, the supported API version
range, its load state and an optional documentation URL.
The scan result for third-party plugins is cached. On a cache hit only
the known-good plugins are required and registered; plugins that failed
to load are not retried and their metadata is served from the cache.
This also means the warning printed when a plugin fails to load is
emitted once when the cache is rebuilt, instead of by every short-lived
process (pvesm, pct, qm, ...).
Additionally the cluster resource endpoint emits an entry for storages
with an unloadable plugin instead of dropping them and the GUI uses
the new metadata to:
- mark outdated and failed storages with a distinct icon in the
resource tree
- show the plugin state, whether it's external, and a link to the
plugin's documentation in the storage status panel
- display a dedicated summary for storages whose plugin could not be
loaded, explaining the problem and pointing to the plugin's homepage
if provided.
Tested on a single node with sshfs with API version 11
(outdated, still loads) and a plugin with an error (load-error, api
too old), plus a storage whose type has no plugin installed at all.
Verified the tree icons, the new StatusView rows, and the dedicated
summary view, and that healthy storages are unchanged.
pve-storage:
Jakob Klocker (5):
storage: drop unused variable in storage_info
storage: add `plugin-url` to built-in plugins
storage: add `unknown` flag to parse_config
storage: plugins: cache and classify custom storage plugins
storage: expose plugin metadata in storage status
src/PVE/API2/Storage/Status.pm | 39 +++++
src/PVE/Storage.pm | 248 +++++++++++++++++++++++----
src/PVE/Storage/BTRFSPlugin.pm | 1 +
src/PVE/Storage/CIFSPlugin.pm | 1 +
src/PVE/Storage/CephFSPlugin.pm | 1 +
src/PVE/Storage/DirPlugin.pm | 1 +
src/PVE/Storage/ISCSIDirectPlugin.pm | 1 +
src/PVE/Storage/ISCSIPlugin.pm | 1 +
src/PVE/Storage/LVMPlugin.pm | 1 +
src/PVE/Storage/LvmThinPlugin.pm | 1 +
src/PVE/Storage/NFSPlugin.pm | 1 +
src/PVE/Storage/PBSPlugin.pm | 1 +
src/PVE/Storage/Plugin.pm | 4 +-
src/PVE/Storage/RBDPlugin.pm | 1 +
src/PVE/Storage/ZFSPlugin.pm | 1 +
src/PVE/Storage/ZFSPoolPlugin.pm | 1 +
16 files changed, 264 insertions(+), 40 deletions(-)
pve-manager:
Jakob Klocker (4):
cluster: backend: include storages with unloadable plugins
www: storage: show plugin state/external/url in StatusView
www: storage: distinguish failed/outdated storage plugins in resource
tree
www: storage: display custom summary for failed plugins
PVE/API2/Cluster.pm | 51 ++++++++++++-
PVE/API2Tools.pm | 6 ++
www/manager6/Makefile | 1 +
www/manager6/Utils.js | 72 ++++++++++++++++++
www/manager6/storage/Browser.js | 33 ++++++---
www/manager6/storage/PluginError.js | 111 ++++++++++++++++++++++++++++
www/manager6/storage/StatusView.js | 38 +++++++++-
7 files changed, 300 insertions(+), 12 deletions(-)
create mode 100644 www/manager6/storage/PluginError.js
Summary over all repositories:
23 files changed, 564 insertions(+), 52 deletions(-)
--
Generated by murpp 0.12.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH pve-storage 1/9] storage: drop unused variable in storage_info
2026-07-31 15:36 [PATCH manager/storage 0/9] storage: plugins: display failed and outdated storage plugins in the GUI Jakob Klocker
@ 2026-07-31 15:36 ` Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 2/9] storage: add `plugin-url` to built-in plugins Jakob Klocker
` (7 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Jakob Klocker @ 2026-07-31 15:36 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
The 'slist' array was populated but never read.
Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
src/PVE/Storage.pm | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index 64ea9da..96579a4 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -1446,7 +1446,6 @@ sub storage_info {
my @ctypes = PVE::Tools::split_list($content);
- my $slist = [];
foreach my $storeid (keys %$ids) {
my $storage_enabled = defined(storage_check_enabled($cfg, $storeid, undef, 1));
@@ -1474,8 +1473,6 @@ sub storage_info {
active => 0,
enabled => $storage_enabled ? 1 : 0,
};
-
- push @$slist, $storeid;
}
my $cache = {};
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH pve-storage 2/9] storage: add `plugin-url` to built-in plugins
2026-07-31 15:36 [PATCH manager/storage 0/9] storage: plugins: display failed and outdated storage plugins in the GUI Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 1/9] storage: drop unused variable in storage_info Jakob Klocker
@ 2026-07-31 15:36 ` Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 3/9] storage: add `unknown` flag to parse_config Jakob Klocker
` (6 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Jakob Klocker @ 2026-07-31 15:36 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
Add documentation URLs for built-in storage plugins so the frontend can
link to each plugin's documentation.
Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
src/PVE/Storage/BTRFSPlugin.pm | 1 +
src/PVE/Storage/CIFSPlugin.pm | 1 +
src/PVE/Storage/CephFSPlugin.pm | 1 +
src/PVE/Storage/DirPlugin.pm | 1 +
src/PVE/Storage/ISCSIDirectPlugin.pm | 1 +
src/PVE/Storage/ISCSIPlugin.pm | 1 +
src/PVE/Storage/LVMPlugin.pm | 1 +
src/PVE/Storage/LvmThinPlugin.pm | 1 +
src/PVE/Storage/NFSPlugin.pm | 1 +
src/PVE/Storage/PBSPlugin.pm | 1 +
src/PVE/Storage/RBDPlugin.pm | 1 +
src/PVE/Storage/ZFSPlugin.pm | 1 +
src/PVE/Storage/ZFSPoolPlugin.pm | 1 +
13 files changed, 13 insertions(+)
diff --git a/src/PVE/Storage/BTRFSPlugin.pm b/src/PVE/Storage/BTRFSPlugin.pm
index fb47aa0..cb1b03e 100644
--- a/src/PVE/Storage/BTRFSPlugin.pm
+++ b/src/PVE/Storage/BTRFSPlugin.pm
@@ -46,6 +46,7 @@ sub plugindata {
],
format => [{ raw => 1, subvol => 1 }, 'raw'],
'sensitive-properties' => {},
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_BTRFS",
};
}
diff --git a/src/PVE/Storage/CIFSPlugin.pm b/src/PVE/Storage/CIFSPlugin.pm
index 54f0f4e..423afc9 100644
--- a/src/PVE/Storage/CIFSPlugin.pm
+++ b/src/PVE/Storage/CIFSPlugin.pm
@@ -126,6 +126,7 @@ sub plugindata {
],
format => [{ raw => 1, qcow2 => 1, vmdk => 1 }, 'raw'],
'sensitive-properties' => { password => 1 },
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_CIFS",
};
}
diff --git a/src/PVE/Storage/CephFSPlugin.pm b/src/PVE/Storage/CephFSPlugin.pm
index fbc9711..ab3c45f 100644
--- a/src/PVE/Storage/CephFSPlugin.pm
+++ b/src/PVE/Storage/CephFSPlugin.pm
@@ -119,6 +119,7 @@ sub plugindata {
content =>
[{ vztmpl => 1, iso => 1, backup => 1, snippets => 1, import => 1 }, { backup => 1 }],
'sensitive-properties' => { keyring => 1 },
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_CephFS",
};
}
diff --git a/src/PVE/Storage/DirPlugin.pm b/src/PVE/Storage/DirPlugin.pm
index 80c4a03..0a332ca 100644
--- a/src/PVE/Storage/DirPlugin.pm
+++ b/src/PVE/Storage/DirPlugin.pm
@@ -39,6 +39,7 @@ sub plugindata {
],
format => [{ raw => 1, qcow2 => 1, vmdk => 1, subvol => 1 }, 'raw'],
'sensitive-properties' => {},
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_Directory",
};
}
diff --git a/src/PVE/Storage/ISCSIDirectPlugin.pm b/src/PVE/Storage/ISCSIDirectPlugin.pm
index f976c31..e60e000 100644
--- a/src/PVE/Storage/ISCSIDirectPlugin.pm
+++ b/src/PVE/Storage/ISCSIDirectPlugin.pm
@@ -67,6 +67,7 @@ sub plugindata {
content => [{ images => 1, none => 1 }, { images => 1 }],
select_existing => 1,
'sensitive-properties' => {},
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_User_Mode_iSCSI",
};
}
diff --git a/src/PVE/Storage/ISCSIPlugin.pm b/src/PVE/Storage/ISCSIPlugin.pm
index 801b5d1..9571ceb 100644
--- a/src/PVE/Storage/ISCSIPlugin.pm
+++ b/src/PVE/Storage/ISCSIPlugin.pm
@@ -335,6 +335,7 @@ sub plugindata {
content => [{ images => 1, none => 1 }, { images => 1 }],
select_existing => 1,
'sensitive-properties' => {},
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_iSCSI",
};
}
diff --git a/src/PVE/Storage/LVMPlugin.pm b/src/PVE/Storage/LVMPlugin.pm
index b8646c2..a774246 100644
--- a/src/PVE/Storage/LVMPlugin.pm
+++ b/src/PVE/Storage/LVMPlugin.pm
@@ -409,6 +409,7 @@ sub plugindata {
content => [{ images => 1, rootdir => 1 }, { images => 1 }],
format => [{ raw => 1, qcow2 => 1 }, 'raw'],
'sensitive-properties' => {},
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_LVM",
};
}
diff --git a/src/PVE/Storage/LvmThinPlugin.pm b/src/PVE/Storage/LvmThinPlugin.pm
index cadf343..d1337ba 100644
--- a/src/PVE/Storage/LvmThinPlugin.pm
+++ b/src/PVE/Storage/LvmThinPlugin.pm
@@ -33,6 +33,7 @@ sub plugindata {
return {
content => [{ images => 1, rootdir => 1 }, { images => 1, rootdir => 1 }],
'sensitive-properties' => {},
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_LVM_Thin",
};
}
diff --git a/src/PVE/Storage/NFSPlugin.pm b/src/PVE/Storage/NFSPlugin.pm
index 4cc02c9..2e046ec 100644
--- a/src/PVE/Storage/NFSPlugin.pm
+++ b/src/PVE/Storage/NFSPlugin.pm
@@ -67,6 +67,7 @@ sub plugindata {
],
format => [{ raw => 1, qcow2 => 1, vmdk => 1 }, 'raw'],
'sensitive-properties' => {},
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_NFS",
};
}
diff --git a/src/PVE/Storage/PBSPlugin.pm b/src/PVE/Storage/PBSPlugin.pm
index 6b049b4..a77d2cd 100644
--- a/src/PVE/Storage/PBSPlugin.pm
+++ b/src/PVE/Storage/PBSPlugin.pm
@@ -36,6 +36,7 @@ sub plugindata {
'master-pubkey' => 1,
password => 1,
},
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_Proxmox_Backup_Server",
};
}
diff --git a/src/PVE/Storage/RBDPlugin.pm b/src/PVE/Storage/RBDPlugin.pm
index b537425..818ce4e 100644
--- a/src/PVE/Storage/RBDPlugin.pm
+++ b/src/PVE/Storage/RBDPlugin.pm
@@ -413,6 +413,7 @@ sub plugindata {
return {
content => [{ images => 1, rootdir => 1 }, { images => 1 }],
'sensitive-properties' => { keyring => 1 },
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_RBD",
};
}
diff --git a/src/PVE/Storage/ZFSPlugin.pm b/src/PVE/Storage/ZFSPlugin.pm
index 74e0a08..f382e8a 100644
--- a/src/PVE/Storage/ZFSPlugin.pm
+++ b/src/PVE/Storage/ZFSPlugin.pm
@@ -177,6 +177,7 @@ sub plugindata {
return {
content => [{ images => 1 }, { images => 1 }],
'sensitive-properties' => {},
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_ZFS_over_ISCSI",
};
}
diff --git a/src/PVE/Storage/ZFSPoolPlugin.pm b/src/PVE/Storage/ZFSPoolPlugin.pm
index 55f0bbc..415140d 100644
--- a/src/PVE/Storage/ZFSPoolPlugin.pm
+++ b/src/PVE/Storage/ZFSPoolPlugin.pm
@@ -57,6 +57,7 @@ sub plugindata {
content => [{ images => 1, rootdir => 1 }, { images => 1, rootdir => 1 }],
format => [{ raw => 1, subvol => 1 }, 'raw'],
'sensitive-properties' => {},
+ 'plugin-url' => "https://pve.proxmox.com/wiki/Storage:_ZFS",
};
}
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH pve-storage 3/9] storage: add `unknown` flag to parse_config
2026-07-31 15:36 [PATCH manager/storage 0/9] storage: plugins: display failed and outdated storage plugins in the GUI Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 1/9] storage: drop unused variable in storage_info Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 2/9] storage: add `plugin-url` to built-in plugins Jakob Klocker
@ 2026-07-31 15:36 ` Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 4/9] storage: plugins: cache and classify custom storage plugins Jakob Klocker
` (5 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Jakob Klocker @ 2026-07-31 15:36 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
While the base class provides an `unknown` flag it wasn't possible to
get unknown storages through the storage parse_config function. With
the added flag it is now possible to access these.
Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
src/PVE/Storage.pm | 6 +++++-
src/PVE/Storage/Plugin.pm | 4 ++--
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index 96579a4..4efc3de 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -230,7 +230,11 @@ my $allowed_qemu_blockdev_options = {
# PVE::Storage utility functions
sub config {
- return cfs_read_file("storage.cfg");
+ my ($allow_unknown) = @_;
+ return cfs_read_file("storage.cfg") if !$allow_unknown;
+
+ my $raw = PVE::Cluster::get_config("storage.cfg");
+ return PVE::Storage::Plugin->parse_config("/etc/pve/storage.cfg", $raw, $allow_unknown);
}
sub write_config {
diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index 4f69f9b..9d79fad 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -557,9 +557,9 @@ sub encode_value {
}
sub parse_config {
- my ($class, $filename, $raw) = @_;
+ my ($class, $filename, $raw, $unknown) = @_;
- my $cfg = $class->SUPER::parse_config($filename, $raw);
+ my $cfg = $class->SUPER::parse_config($filename, $raw, $unknown);
my $ids = $cfg->{ids};
# make sure we have a reasonable 'local:' storage
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH pve-storage 4/9] storage: plugins: cache and classify custom storage plugins
2026-07-31 15:36 [PATCH manager/storage 0/9] storage: plugins: display failed and outdated storage plugins in the GUI Jakob Klocker
` (2 preceding siblings ...)
2026-07-31 15:36 ` [PATCH pve-storage 3/9] storage: add `unknown` flag to parse_config Jakob Klocker
@ 2026-07-31 15:36 ` Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 5/9] storage: expose plugin metadata in storage status Jakob Klocker
` (4 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Jakob Klocker @ 2026-07-31 15:36 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
Loading custom storage plugins happens as a side effect of 'use
PVE::Storage', so every short-lived process (pvesm, qm, pct,..) repeats
the full directory scan, require and register cycle. For a plugin that
fails to load this also meant the warning was printed again for every
single invocation.
Information about failed plugins was also lost as soon as the process
exited, since it only went to STDERR via warn. This made it impossible
to surface through the API.
Store the scan result in /run/pve/storage-plugin-status, keyed by a
fingerprint over the plugin files and classify each plugin with a state
('up-to-date', 'outdated', 'load-error'). On a cache hit only the known
"good" plugins are re-required and registered; failed plugins are not
loaded again and their metadata is served from the cache. The warning is
now emitted once at cache rebuild time instead of once per process.
Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
src/PVE/Storage.pm | 223 ++++++++++++++++++++++++++++++++++++++-------
1 file changed, 189 insertions(+), 34 deletions(-)
diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index 4efc3de..b433efc 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -3,6 +3,7 @@ package PVE::Storage;
use strict;
use warnings;
+use JSON;
use POSIX;
use IO::Select;
use IO::File;
@@ -12,6 +13,7 @@ use File::Basename;
use File::Path;
use Cwd 'abs_path';
use Socket;
+use Digest::SHA qw(sha256_hex);
use Time::Local qw(timelocal);
use PVE::Tools qw(run_command file_read_firstline dir_glob_foreach $IPV6RE);
@@ -65,46 +67,199 @@ PVE::Storage::PBSPlugin->register();
PVE::Storage::BTRFSPlugin->register();
PVE::Storage::ESXiPlugin->register();
-# load third-party plugins
-if (-d '/usr/share/perl5/PVE/Storage/Custom') {
+my $CUSTOM_PLUGIN_DIR = '/usr/share/perl5/PVE/Storage/Custom';
+my $PLUGIN_STATUS_CACHE = '/run/pve/storage-plugin-status';
+
+my $STATE_UPTODATE = 'up-to-date';
+my $STATE_OUTDATED = 'outdated';
+my $STATE_LOAD_ERROR = 'load-error';
+
+my $custom_plugin_status = eval { load_custom_plugins() } // { failed => {}, loaded => {} };
+warn "loading custom storage plugins failed: $@\n" if $@;
+
+sub load_custom_plugins {
+ my $fingerprint = custom_plugin_fingerprint();
+
+ my $cached = eval { decode_json(PVE::Tools::file_get_contents($PLUGIN_STATUS_CACHE)); };
+ my $cache_age = defined($cached) ? (time - ((stat($PLUGIN_STATUS_CACHE))[9] // 0)) : undef;
+ my $fresh =
+ $cached
+ && ($cached->{fingerprint} // '') eq $fingerprint
+ && defined($cache_age)
+ && $cache_age < 3600;
+
+ if ($fresh) {
+ register_cached_custom_plugins($cached->{loaded});
+ return $cached;
+ }
+
+ return PVE::Tools::lock_file(
+ "$PLUGIN_STATUS_CACHE.lock",
+ 10,
+ sub {
+ # another process may have rebuilt the cache while waiting for the lock, check again
+ my $again =
+ eval { decode_json(PVE::Tools::file_get_contents($PLUGIN_STATUS_CACHE)); };
+ my $again_age =
+ defined($again) ? (time - ((stat($PLUGIN_STATUS_CACHE))[9] // 0)) : undef;
+ if (
+ $again
+ && ($again->{fingerprint} // '') eq $fingerprint
+ && defined($again_age)
+ && $again_age < 3600
+ ) {
+ register_cached_custom_plugins($again->{loaded});
+ return $again;
+ }
+
+ my $result = scan_and_register_custom_plugins();
+ $result->{fingerprint} = $fingerprint;
+
+ mkdir '/run/pve' if !-d '/run/pve';
+ PVE::Tools::file_set_contents($PLUGIN_STATUS_CACHE, encode_json($result));
+
+ return $result;
+ },
+ );
+}
+
+sub scan_and_register_custom_plugins {
+ my $failed = {};
+ my $loaded = {};
+
+ if (-d $CUSTOM_PLUGIN_DIR) {
+ dir_glob_foreach(
+ $CUSTOM_PLUGIN_DIR,
+ '.*\.pm$',
+ sub {
+ my ($file) = @_;
+ my $modname = 'PVE::Storage::Custom::' . $file;
+ $modname =~ s!\.pm$!!;
+ my $requiername = 'PVE/Storage/Custom/' . $file;
+
+ my ($version, $min_version);
+ my $state = $STATE_LOAD_ERROR;
+
+ eval {
+ require $requiername;
+
+ 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');
+
+ $version = $modname->api();
+ $min_version = (APIVER - APIAGE);
+
+ if ($version > APIVER) {
+ $state = $STATE_LOAD_ERROR;
+ die "implements an API version newer than current ($version > "
+ . APIVER . ")\n";
+ }
+ if ($version < $min_version) {
+ $state = $STATE_LOAD_ERROR;
+ die "API version too old, please update the plugin "
+ . "($version < $min_version)\n";
+ }
+
+ import $requiername;
+ $modname->register();
+
+ my $plugindata = $modname->plugindata();
+ my $content = eval {
+ {
+ supported => [sort keys $plugindata->{content}->[0]->%*],
+ default => [sort keys $plugindata->{content}->[1]->%*],
+ };
+ } // { supported => [], default => [] };
+
+ $loaded->{$modname} = {
+ modname => $modname,
+ type => scalar($modname->type()),
+ state => $version == APIVER ? $STATE_UPTODATE : $STATE_OUTDATED,
+ content => $content,
+ 'api-version' => $version,
+ 'plugin-url' => $plugindata->{'plugin-url'},
+ };
+ };
+ if (my $err = $@) {
+ my $type = eval { $modname->type() };
+ my $plugindata = eval { $modname->plugindata() } // {};
+ my $content = eval {
+ {
+ supported => [sort keys $plugindata->{content}->[0]->%*],
+ default => [sort keys $plugindata->{content}->[1]->%*],
+ };
+ } // { supported => [], default => [] };
+
+ log_warn("storage plugin '$modname' failed to load: $err");
+
+ $failed->{$modname} = {
+ modname => $modname,
+ type => $type,
+ state => $state,
+ content => $content,
+ 'api-version' => $version,
+ 'plugin-url' => $plugindata->{'plugin-url'},
+ };
+ }
+ },
+ );
+
+ }
+ return { failed => $failed, loaded => $loaded };
+}
+
+sub register_cached_custom_plugins {
+ my ($loaded) = @_;
+ return if !$loaded;
+ for my $modname (sort keys $loaded->%*) {
+ my $file = $modname =~ s!::!/!gr . '.pm';
+ eval {
+ require $file;
+ $modname->register() if $modname->can('register');
+ };
+ warn "re-registering cached custom plugin '$modname' failed: $@" if $@;
+ }
+}
+
+sub custom_plugin_fingerprint {
+ return '' if !-d $CUSTOM_PLUGIN_DIR;
+
+ my @parts;
dir_glob_foreach(
- '/usr/share/perl5/PVE/Storage/Custom',
+ $CUSTOM_PLUGIN_DIR,
'.*\.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\": $@";
- }
+ my ($dev, $ino, $size, $mtime, $ctime) =
+ (stat("$CUSTOM_PLUGIN_DIR/$file"))[0, 1, 7, 9, 10];
+ push @parts, "$file:$dev:$ino:$size:$mtime:$ctime";
},
);
+
+ return sha256_hex(join('|', sort @parts));
+}
+
+sub failed_third_party_plugins {
+ return $custom_plugin_status->{failed};
+}
+
+my sub custom_plugin_entry {
+ my ($type) = @_;
+ for my $group (qw(failed loaded)) {
+ for my $value (values %{ $custom_plugin_status->{$group} }) {
+ return $value if $value->{type} eq $type;
+ }
+ }
+ return undef;
+}
+
+sub get_plugin_state {
+ my ($type) = @_;
+
+ my $entry = custom_plugin_entry($type);
+ # if plugin is a builtin return up-to-date
+ return $entry ? $entry->{state} : $STATE_UPTODATE;
}
# initialize all plugins
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH pve-storage 5/9] storage: expose plugin metadata in storage status
2026-07-31 15:36 [PATCH manager/storage 0/9] storage: plugins: display failed and outdated storage plugins in the GUI Jakob Klocker
` (3 preceding siblings ...)
2026-07-31 15:36 ` [PATCH pve-storage 4/9] storage: plugins: cache and classify custom storage plugins Jakob Klocker
@ 2026-07-31 15:36 ` Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-manager 6/9] cluster: backend: include storages with unloadable plugins Jakob Klocker
` (3 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Jakob Klocker @ 2026-07-31 15:36 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
The plugin state determined when scanning custom plugins was so far only
available internally. Add it to the per-storage information, together
with the API version the plugin implements, the API version range this
PVE supports, whether the plugin is provided by a third party, and the
optional plugin URL.
This allows the GUI to tell users that a storage uses an outdated or
broken plugin and to point them at the plugin's homepage.
Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
src/PVE/API2/Storage/Status.pm | 39 ++++++++++++++++++++++++++++++++++
src/PVE/Storage.pm | 16 ++++++++++++++
2 files changed, 55 insertions(+)
diff --git a/src/PVE/API2/Storage/Status.pm b/src/PVE/API2/Storage/Status.pm
index 741d514..f044e7a 100644
--- a/src/PVE/API2/Storage/Status.pm
+++ b/src/PVE/API2/Storage/Status.pm
@@ -390,6 +390,45 @@ __PACKAGE__->register_method({
renderer => 'bytes',
optional => 1,
},
+ plugin => {
+ description => "Metadata about the storage plugin backing this storage.",
+ type => 'object',
+ optional => 1,
+ properties => {
+ external => {
+ description =>
+ "Set when the storage is provided by a third-party plugin.",
+ type => 'boolean',
+ optional => 1,
+ },
+ state => {
+ description => "Plugin state: up-to-date, outdated, or load-error.",
+ type => 'string',
+ enum => ['up-to-date', 'outdated', 'load-error'],
+ optional => 1,
+ },
+ 'api-version' => {
+ description => "API version implemented by the storage plugin.",
+ type => 'integer',
+ optional => 1,
+ },
+ 'api-min' => {
+ description => "Minimum plugin API version supported by this PVE.",
+ type => 'integer',
+ optional => 1,
+ },
+ 'api-max' => {
+ description => "Current plugin API version of this PVE.",
+ type => 'integer',
+ optional => 1,
+ },
+ url => {
+ description => "URL to the plugin's homepage or documentation.",
+ type => 'string',
+ optional => 1,
+ },
+ },
+ },
},
},
code => sub {
diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index b433efc..06be29d 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -1620,6 +1620,14 @@ sub storage_info {
}
my $type = $ids->{$storeid}->{type};
+ my $entry = custom_plugin_entry($type);
+ my $external = $entry ? 1 : 0;
+ my $state = get_plugin_state($type);
+ my $api_version = $entry ? $entry->{'api-version'} : APIVER;
+ my $plugin_url =
+ $entry
+ ? $entry->{'plugin-url'}
+ : eval { PVE::Storage::Plugin->lookup($type)->plugindata()->{'plugin-url'} };
$info->{$storeid} = {
type => $type,
@@ -1631,6 +1639,14 @@ sub storage_info {
PVE::Storage::Plugin::content_hash_to_string($ids->{$storeid}->{content}),
active => 0,
enabled => $storage_enabled ? 1 : 0,
+ plugin => {
+ external => $external,
+ state => $state,
+ 'api-version' => $api_version,
+ 'api-min' => (APIVER - APIAGE),
+ 'api-max' => APIVER,
+ url => $plugin_url,
+ },
};
}
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH pve-manager 6/9] cluster: backend: include storages with unloadable plugins
2026-07-31 15:36 [PATCH manager/storage 0/9] storage: plugins: display failed and outdated storage plugins in the GUI Jakob Klocker
` (4 preceding siblings ...)
2026-07-31 15:36 ` [PATCH pve-storage 5/9] storage: expose plugin metadata in storage status Jakob Klocker
@ 2026-07-31 15:36 ` Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-manager 7/9] www: storage: show plugin state/external/url in StatusView Jakob Klocker
` (2 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Jakob Klocker @ 2026-07-31 15:36 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
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 <j.klocker@proxmox.com>
---
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
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH pve-manager 7/9] www: storage: show plugin state/external/url in StatusView
2026-07-31 15:36 [PATCH manager/storage 0/9] storage: plugins: display failed and outdated storage plugins in the GUI Jakob Klocker
` (5 preceding siblings ...)
2026-07-31 15:36 ` [PATCH pve-manager 6/9] cluster: backend: include storages with unloadable plugins Jakob Klocker
@ 2026-07-31 15:36 ` Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-manager 8/9] www: storage: distinguish failed/outdated storage plugins in resource tree Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-manager 9/9] www: storage: display custom summary for failed plugins Jakob Klocker
8 siblings, 0 replies; 10+ messages in thread
From: Jakob Klocker @ 2026-07-31 15:36 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
Display additional storage information: `Plugin State`, `External` and
`Plugin URL`. The `Plugin URL` row is hidden when no URL is set, and
third-party plugin URLs are prefixed to distinguish them from official
documentation links. The `Plugin State` row shows whether the plugin is
up to date, outdated (with how many more API versions it will remain
supported), or failed to load.
Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
www/manager6/Utils.js | 68 ++++++++++++++++++++++++++++++
www/manager6/storage/StatusView.js | 38 ++++++++++++++++-
2 files changed, 105 insertions(+), 1 deletion(-)
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 040b5ae0..2f098ff0 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1072,6 +1072,32 @@ Ext.define('PVE.Utils', {
.join(', ');
},
+ format_plugin_state: function (value) {
+ if (!value) {
+ return '';
+ }
+ switch (value.state) {
+ case 'load-error':
+ return gettext('Failed to load');
+ case 'outdated': {
+ let ver = value['api-version'];
+ let min = value['api-min'];
+ let max = value['api-max'];
+ let headroom = ver - min;
+ return Ext.String.format(
+ gettext(
+ 'Outdated (API {0}, current {1}) - supported for {2} more API version(s)',
+ ),
+ ver,
+ max,
+ headroom,
+ );
+ }
+ default:
+ return gettext('Up to date');
+ }
+ },
+
render_storage_content: function (value, metaData, record) {
let data = record.data;
let result;
@@ -1367,6 +1393,48 @@ Ext.define('PVE.Utils', {
return Ext.htmlEncode(Proxmox.Utils.format_task_description(type, id));
},
+ render_plugin_url: function (value, type) {
+ if (value === undefined || value === null || value === '') {
+ return '';
+ }
+ let isLink = /^https?:\/\//i.test(value);
+ if (!isLink) {
+ return Ext.htmlEncode(value);
+ }
+
+ let isOfficial = false;
+
+ try {
+ isOfficial = new URL(value).hostname === 'pve.proxmox.com';
+ } catch {
+ isOfficial = false;
+ }
+
+ let encoded = Ext.htmlEncode(value);
+
+ if (isOfficial) {
+ let name = type ? PVE.Utils.format_storage_type(type) : '';
+ let label = name
+ ? Ext.String.format(gettext('{0} Documentation'), name)
+ : gettext('Documentation');
+ return (
+ '<a href="' +
+ encoded +
+ '" target="_blank" rel="noopener noreferrer">' +
+ Ext.htmlEncode(label) +
+ '</a>'
+ );
+ }
+
+ let link =
+ '<a href="' +
+ encoded +
+ '" target="_blank" rel="noopener noreferrer">' +
+ encoded +
+ '</a>';
+ return gettext('(External) ') + link;
+ },
+
render_optional_url: function (value) {
if (value && value.match(/^https?:\/\//)) {
return '<a target="_blank" href="' + value + '">' + value + '</a>';
diff --git a/www/manager6/storage/StatusView.js b/www/manager6/storage/StatusView.js
index 0525fbb5..ba5e6b07 100644
--- a/www/manager6/storage/StatusView.js
+++ b/www/manager6/storage/StatusView.js
@@ -2,7 +2,7 @@ Ext.define('PVE.storage.StatusView', {
extend: 'Proxmox.panel.StatusView',
alias: 'widget.pveStorageStatusView',
- height: 230,
+ height: 320,
title: gettext('Status'),
layout: {
@@ -47,6 +47,42 @@ Ext.define('PVE.storage.StatusView', {
textField: 'type',
renderer: PVE.Utils.format_storage_type,
},
+ {
+ itemId: 'plugin-external',
+ title: gettext('External'),
+ printBar: false,
+ textField: 'plugin',
+ renderer: (value) => Proxmox.Utils.format_boolean(value?.external),
+ },
+ {
+ itemId: 'plugin-status',
+ title: gettext('Plugin State'),
+ printBar: false,
+ textField: 'plugin',
+ renderer: (value) => PVE.Utils.format_plugin_state(value),
+ },
+ {
+ itemId: 'plugin-url',
+ title: gettext('Plugin URL'),
+ printBar: false,
+ textField: 'plugin',
+ hidden: true,
+ renderer: function (value) {
+ let url = value?.url;
+ let has = url !== undefined && url !== null && url !== '';
+ this.setHidden(!has);
+ if (!has) {
+ return '';
+ }
+ let view = this.up('pveStorageStatusView');
+ let type;
+ if (view && view.rstore) {
+ let rec = view.rstore.getById('type');
+ type = rec ? rec.data.value : undefined;
+ }
+ return PVE.Utils.render_plugin_url(url, type);
+ },
+ },
{
xtype: 'box',
height: 10,
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH pve-manager 8/9] www: storage: distinguish failed/outdated storage plugins in resource tree
2026-07-31 15:36 [PATCH manager/storage 0/9] storage: plugins: display failed and outdated storage plugins in the GUI Jakob Klocker
` (6 preceding siblings ...)
2026-07-31 15:36 ` [PATCH pve-manager 7/9] www: storage: show plugin state/external/url in StatusView Jakob Klocker
@ 2026-07-31 15:36 ` Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-manager 9/9] www: storage: display custom summary for failed plugins Jakob Klocker
8 siblings, 0 replies; 10+ messages in thread
From: Jakob Klocker @ 2026-07-31 15:36 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
Render storage nodes whose plugin failed to load or is outdated with a
distinct icon in the resource tree.
Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
www/manager6/Utils.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 2f098ff0..1cb188c1 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1351,6 +1351,10 @@ Ext.define('PVE.Utils', {
};
return networkTypeMapping[record['network-type']] ?? '';
+ } else if (type === 'storage' && record.pluginstate === 'load-error') {
+ return 'fa fa-database error faded';
+ } else if (type === 'storage' && record.pluginstate === 'outdated') {
+ return 'fa fa-database pending';
} else if (type === 'storage' && record.content === 'import') {
return 'fa fa-cloud-download';
} else {
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH pve-manager 9/9] www: storage: display custom summary for failed plugins
2026-07-31 15:36 [PATCH manager/storage 0/9] storage: plugins: display failed and outdated storage plugins in the GUI Jakob Klocker
` (7 preceding siblings ...)
2026-07-31 15:36 ` [PATCH pve-manager 8/9] www: storage: distinguish failed/outdated storage plugins in resource tree Jakob Klocker
@ 2026-07-31 15:36 ` Jakob Klocker
8 siblings, 0 replies; 10+ messages in thread
From: Jakob Klocker @ 2026-07-31 15:36 UTC (permalink / raw)
To: pve-devel; +Cc: Jakob Klocker
Currently there is only a generic summary for storages, which doesn't
mention that a plugin failed to load.
Add a dedicated summary view for storages whose plugin failed to load.
Signed-off-by: Jakob Klocker <j.klocker@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/storage/Browser.js | 33 ++++++---
www/manager6/storage/PluginError.js | 111 ++++++++++++++++++++++++++++
3 files changed, 136 insertions(+), 9 deletions(-)
create mode 100644 www/manager6/storage/PluginError.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index eb0e9d9c..7eb187e9 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -370,6 +370,7 @@ JSSRC= \
storage/BTRFSEdit.js \
storage/NFSEdit.js \
storage/PBSEdit.js \
+ storage/PluginError.js \
storage/RBDEdit.js \
storage/StatusView.js \
storage/Summary.js \
diff --git a/www/manager6/storage/Browser.js b/www/manager6/storage/Browser.js
index d0237948..2e3cfb42 100644
--- a/www/manager6/storage/Browser.js
+++ b/www/manager6/storage/Browser.js
@@ -29,6 +29,30 @@ Ext.define('PVE.storage.Browser', {
let plugin = res.plugintype;
let isEsxi = plugin === 'esxi';
+ let pluginFailed = res.pluginstate === 'load-error';
+
+ Ext.apply(me, {
+ title: Ext.String.format(
+ gettext('Storage {0} on node {1}'),
+ `'${storeid}'`,
+ `'${nodename}'`,
+ ),
+ hstateid: 'storagetab',
+ });
+
+ if (pluginFailed) {
+ me.items = [
+ {
+ xtype: 'pveStoragePluginError',
+ title: gettext('Summary'),
+ iconCls: 'fa fa-book',
+ itemId: 'summary',
+ },
+ ];
+
+ me.callParent();
+ return;
+ }
me.items = !isEsxi
? [
@@ -43,15 +67,6 @@ Ext.define('PVE.storage.Browser', {
let caps = Ext.state.Manager.get('GuiCap');
- Ext.apply(me, {
- title: Ext.String.format(
- gettext('Storage {0} on node {1}'),
- `'${storeid}'`,
- `'${nodename}'`,
- ),
- hstateid: 'storagetab',
- });
-
if (
caps.storage['Datastore.Allocate'] ||
caps.storage['Datastore.AllocateSpace'] ||
diff --git a/www/manager6/storage/PluginError.js b/www/manager6/storage/PluginError.js
new file mode 100644
index 00000000..a797631a
--- /dev/null
+++ b/www/manager6/storage/PluginError.js
@@ -0,0 +1,111 @@
+Ext.define('PVE.storage.PluginError', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveStoragePluginError',
+
+ scrollable: true,
+ bodyPadding: 5,
+ layout: { type: 'column' },
+ defaults: { padding: 5, columnWidth: 1 },
+
+ initComponent: function () {
+ let me = this;
+
+ let nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw 'no node name specified';
+ }
+ let storage = me.pveSelNode.data.storage;
+ if (!storage) {
+ throw 'no storage ID specified';
+ }
+
+ let res = me.pveSelNode.data;
+ let type = res.plugintype || gettext('unknown');
+ let content = res.content || '-';
+
+ let rows = [
+ {
+ xtype: 'pmxInfoWidget',
+ itemId: 'enabled',
+ title: gettext('Enabled'),
+ printBar: false,
+ },
+ { xtype: 'pmxInfoWidget', itemId: 'active', title: gettext('Active'), printBar: false },
+ {
+ xtype: 'pmxInfoWidget',
+ itemId: 'content',
+ title: gettext('Content'),
+ printBar: false,
+ },
+ { xtype: 'pmxInfoWidget', itemId: 'type', title: gettext('Type'), printBar: false },
+ {
+ xtype: 'pmxInfoWidget',
+ itemId: 'external',
+ title: gettext('External'),
+ printBar: false,
+ },
+ {
+ xtype: 'pmxInfoWidget',
+ itemId: 'plugstatus',
+ title: gettext('Plugin Status'),
+ printBar: false,
+ },
+ {
+ xtype: 'pmxInfoWidget',
+ itemId: 'plugin-url',
+ title: gettext('Plugin URL'),
+ printBar: false,
+ hidden: true,
+ },
+ ];
+
+ let explanation = Ext.String.format(
+ gettext(
+ "The plugin for storage type '{0}' could not be loaded on node '{1}', " +
+ 'so live status and usage are unavailable. Install or update a compatible ' +
+ 'version of the plugin to restore access.',
+ ),
+ Ext.htmlEncode(type),
+ Ext.htmlEncode(nodename),
+ );
+
+ Ext.apply(me, {
+ items: [
+ {
+ xtype: 'panel',
+ title: gettext('Status'),
+ layout: { type: 'vbox', align: 'stretch' },
+ bodyPadding: 5,
+ defaults: { padding: '2 25' },
+ items: rows,
+ listeners: {
+ afterrender: function (panel) {
+ panel.down('#enabled').updateValue(gettext('No'));
+ panel.down('#active').updateValue(gettext('No'));
+ panel
+ .down('#content')
+ .updateValue(PVE.Utils.format_content_types(content));
+ panel.down('#type').updateValue(PVE.Utils.format_storage_type(type));
+ panel.down('#external').updateValue(gettext('Yes'));
+ panel.down('#plugstatus').updateValue(gettext('Error loading Plugin'));
+
+ let url = res['plugin-url'];
+ if (url) {
+ let row = panel.down('#plugin-url');
+ row.updateValue(PVE.Utils.render_plugin_url(url, res.plugintype));
+ row.setHidden(false);
+ }
+ },
+ },
+ },
+ {
+ xtype: 'box',
+ padding: 15,
+ html: explanation,
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-07-31 15:37 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-31 15:36 [PATCH manager/storage 0/9] storage: plugins: display failed and outdated storage plugins in the GUI Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 1/9] storage: drop unused variable in storage_info Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 2/9] storage: add `plugin-url` to built-in plugins Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 3/9] storage: add `unknown` flag to parse_config Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 4/9] storage: plugins: cache and classify custom storage plugins Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-storage 5/9] storage: expose plugin metadata in storage status Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-manager 6/9] cluster: backend: include storages with unloadable plugins Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-manager 7/9] www: storage: show plugin state/external/url in StatusView Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-manager 8/9] www: storage: distinguish failed/outdated storage plugins in resource tree Jakob Klocker
2026-07-31 15:36 ` [PATCH pve-manager 9/9] www: storage: display custom summary for failed plugins Jakob Klocker
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox