public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins
@ 2026-07-17 15:49 Max R. Carrara
  2026-07-17 15:49 ` [PATCH pve-common v2 01/17] json schema: add multiline string format Max R. Carrara
                   ` (18 more replies)
  0 siblings, 19 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

GUI Support for Custom Storage Plugins - v2
===========================================

Notable Changes Since v1
------------------------

- Fix the 'sensitive' keyword being stringified to `"1"` when enabled,
  instead of being an integer.
  (Thanks @Jakob!)

- Fix a non-breaking hyphen sneaking in, most likely caused by me
  copying an already existing string from the UI or some other place.
  (Thanks @Jakob!)

- Implement *experimental* support for `patternProperties` in order to
  support the custom schema keywords `x-advanced` and `x-hidden`, which
  helps to avoid hard-coding which fields are in the "Advanced" section
  and which ones are hidden in the UI, respectively.

  Please note that the commits implementing this are explicitly marked
  as RFC and can be dropped if `patternProperties` is undesired.

  I figured that this was a decent use case for `patternProperties`, but
  I'm on the fence whether this is something that we actually want.
  Feedback is very welcome in that regard.

  Note that the 'sensitive' keyword that's added to the returned plugin
  schema is kept as is at the moment -- I'm a bit on the fence here too
  whether it should be an 'x-' keyword or whether we should actually
  include it in the default schema for JSON schemas overall, as I can
  see it being useful in other places in the future too, e.g. the ACME
  plugin code (for hiding API keys etc.).


Also, given that the two fixes above are rather trivial and no
functionality has been changed, I suggest keeping Jakob's trailer(s)
from v1 in, should this series get applied.

Previous Versions
-----------------

rfc-v1: https://lore.proxmox.com/pve-devel/20250908180058.530119-1-m.carrara@proxmox.com/
rfc-v2: https://lore.proxmox.com/pve-devel/20251121165858.818307-1-m.carrara@proxmox.com/
v1: https://lore.proxmox.com/pve-devel/20260623143402.772452-1-m.carrara@proxmox.com/

Summary of Changes
------------------


pve-common:

Max R. Carrara (2):
  json schema: add multiline string format
  jsonschema: support 'patternProperties' and allow 'x-' keywords

 src/PVE/JSONSchema.pm    | 66 +++++++++++++++++++++++++++++++++++-----
 test/json-schema-test.pl |  2 +-
 2 files changed, 60 insertions(+), 8 deletions(-)


pve-storage:

Max R. Carrara (8):
  api: plugins/storage: add initial routes and endpoints
  api: plugins/storage/plugin: include schema in plugin metadata
  api: plugins/storage/plugin: mark sensitive properties in schema
  api: plugins/storage/plugin: factor plugin metadata code into helper
  api: plugins/storage/plugin: add plugins' 'content' to their metadata
  all plugins: add 'title' to properties, adapt 'description's
  plugin: mark 'preallocation' and 'content-dirs' properties as advanced
  plugin, dirplugin: mark certain properties as hidden

 src/PVE/API2/Makefile                  |   1 +
 src/PVE/API2/Plugins/Makefile          |  18 +++
 src/PVE/API2/Plugins/Storage.pm        |  54 ++++++++
 src/PVE/API2/Plugins/Storage/Makefile  |  17 +++
 src/PVE/API2/Plugins/Storage/Plugin.pm | 163 +++++++++++++++++++++++++
 src/PVE/Storage/BTRFSPlugin.pm         |   1 +
 src/PVE/Storage/CIFSPlugin.pm          |  10 +-
 src/PVE/Storage/CephFSPlugin.pm        |   6 +-
 src/PVE/Storage/DirPlugin.pm           |  19 ++-
 src/PVE/Storage/ESXiPlugin.pm          |   1 +
 src/PVE/Storage/ISCSIPlugin.pm         |   6 +-
 src/PVE/Storage/LVMPlugin.pm           |  10 +-
 src/PVE/Storage/LvmThinPlugin.pm       |   3 +-
 src/PVE/Storage/NFSPlugin.pm           |   6 +-
 src/PVE/Storage/PBSPlugin.pm           |   5 +-
 src/PVE/Storage/Plugin.pm              |  50 ++++++--
 src/PVE/Storage/RBDPlugin.pm           |  16 ++-
 src/PVE/Storage/ZFSPlugin.pm           |  16 ++-
 src/PVE/Storage/ZFSPoolPlugin.pm       |   9 +-
 19 files changed, 374 insertions(+), 37 deletions(-)
 create mode 100644 src/PVE/API2/Plugins/Makefile
 create mode 100644 src/PVE/API2/Plugins/Storage.pm
 create mode 100644 src/PVE/API2/Plugins/Storage/Makefile
 create mode 100644 src/PVE/API2/Plugins/Storage/Plugin.pm


proxmox-widget-toolkit:

Max R. Carrara (3):
  form: introduce new 'proxmoxtextarea' field
  utils: introduce helper function getFieldDefFromPropertySchema
  acme: use helper to construct ExtJS fields from property schemas

 src/Utils.js                 | 150 +++++++++++++++++++++++++++++++++++
 src/form/TextAreaField.js    |  47 +++++++++++
 src/window/ACMEPluginEdit.js |  42 +++-------
 3 files changed, 210 insertions(+), 29 deletions(-)


pve-manager:

Max R. Carrara (4):
  api: add API routes 'plugins' and 'plugins/storage'
  ui: storage view: display error when no editor for storage type exists
  ui: storage: add basic UI integration for custom storage plugins
  ui: storage: use property extension keywords for UI hints

 PVE/API2.pm                        |   6 +
 PVE/API2/Makefile                  |   1 +
 PVE/API2/Plugins.pm                |  61 +++++++++
 www/manager6/Makefile              |   1 +
 www/manager6/dc/StorageView.js     | 132 ++++++++++++++-----
 www/manager6/storage/Base.js       |   2 +
 www/manager6/storage/CustomEdit.js | 196 +++++++++++++++++++++++++++++
 7 files changed, 365 insertions(+), 34 deletions(-)
 create mode 100644 PVE/API2/Plugins.pm
 create mode 100644 www/manager6/storage/CustomEdit.js


Summary over all repositories:
  31 files changed, 1009 insertions(+), 108 deletions(-)

--
Generated by murpp 0.11.0




^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH pve-common v2 01/17] json schema: add multiline string format
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [RFC pve-common v2 02/17] jsonschema: support 'patternProperties' and allow 'x-' keywords Max R. Carrara
                   ` (17 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Add a new format named "multiline".

This format explicitly signals to other components (e.g. a client)
that a string is a multiline string and should remain one, without
folding newlines or replacing them with spaces, for example.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/JSONSchema.pm | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/PVE/JSONSchema.pm b/src/PVE/JSONSchema.pm
index 1a3583e..49f6aa6 100644
--- a/src/PVE/JSONSchema.pm
+++ b/src/PVE/JSONSchema.pm
@@ -235,6 +235,11 @@ sub get_renderer {
 
 register_format('string', sub { }); # allow format => 'string-list'
 
+# A multiline string.
+# Used to explicitly tell other components that a string should not
+# have its newlines folded or replaced with spaces.
+register_format('multiline', sub { });
+
 register_format('urlencoded', \&pve_verify_urlencoded);
 
 sub pve_verify_urlencoded {
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [RFC pve-common v2 02/17] jsonschema: support 'patternProperties' and allow 'x-' keywords
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
  2026-07-17 15:49 ` [PATCH pve-common v2 01/17] json schema: add multiline string format Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-20 12:01   ` Jakob Klocker
  2026-07-17 15:49 ` [PATCH pve-storage v2 03/17] api: plugins/storage: add initial routes and endpoints Max R. Carrara
                   ` (16 subsequent siblings)
  18 siblings, 1 reply; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

by adding the necessary validation code before the
`additionalProperties` handling in `check_object()` and extending the
default schema (the schema for JSON schemas).

At the same time, allow all keywords beginning with 'x-' to be
included in a given schema. This makes it possible specify custom
extension keywords that can be used for e.g. hints, flags, and other
things.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
NOTE: This patch is experimental and thus marked as RFC.

 src/PVE/JSONSchema.pm    | 61 +++++++++++++++++++++++++++++++++++-----
 test/json-schema-test.pl |  2 +-
 2 files changed, 55 insertions(+), 8 deletions(-)

diff --git a/src/PVE/JSONSchema.pm b/src/PVE/JSONSchema.pm
index 49f6aa6..1bfa5f6 100644
--- a/src/PVE/JSONSchema.pm
+++ b/src/PVE/JSONSchema.pm
@@ -1073,6 +1073,9 @@ sub parse_property_string {
     # In property strings we default to not allowing additional properties
     $additional_properties = 0 if !defined($additional_properties);

+    # In property strings we do not allow pattern properties at all
+    my $pattern_properties = 0;
+
     # Support named formats here, too:
     my $validator;
     if (!ref($format)) {
@@ -1141,7 +1144,7 @@ sub parse_property_string {
     }

     my $errors = {};
-    check_object($path, $format, $res, $additional_properties, $errors);
+    check_object($path, $format, $res, $pattern_properties, $additional_properties, $errors);
     if (scalar(%$errors)) {
         raise "format error\n", errors => $errors;
     }
@@ -1294,7 +1297,7 @@ my sub get_instance_type {
 }

 sub check_object {
-    my ($path, $schema, $value, $additional_properties, $errors) = @_;
+    my ($path, $schema, $value, $pattern_properties, $additional_properties, $errors) = @_;

     # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);

@@ -1367,6 +1370,23 @@ sub check_object {
             next if $matched_type; # value is already checked above
         }

+        if (defined($pattern_properties) && $pattern_properties) {
+            my $has_err = 0;
+            my $pattern_schema = undef;
+
+            for my $pattern (keys $pattern_properties->%*) {
+                if ($k =~ $pattern) {
+                    $pattern_schema = $pattern_properties->{$pattern};
+                    last;
+                }
+            }
+
+            next if $has_err;
+
+            check_prop($value->{$k}, $pattern_schema, $newpath, $errors);
+            next;
+        }
+
         if (defined($additional_properties) && !$additional_properties) {
             add_error(
                 $errors,
@@ -1382,9 +1402,9 @@ sub check_object {
 }

 sub check_object_warn {
-    my ($path, $schema, $value, $additional_properties) = @_;
+    my ($path, $schema, $value, $pattern_properties, $additional_properties) = @_;
     my $errors = {};
-    check_object($path, $schema, $value, $additional_properties, $errors);
+    check_object($path, $schema, $value, $pattern_properties, $additional_properties, $errors);
     if (scalar(%$errors)) {
         foreach my $k (keys %$errors) {
             warn "parse error: $k: $errors->{$k}\n";
@@ -1503,11 +1523,16 @@ sub check_prop {
                 }
             }
             return;
-        } elsif ($schema->{properties} || $schema->{additionalProperties}) {
+        } elsif (
+            $schema->{properties}
+            || $schema->{patternProperties}
+            || $schema->{additionalProperties}
+        ) {
             check_object(
                 $path,
                 defined($schema->{properties}) ? $schema->{properties} : {},
                 $value,
+                $schema->{patternProperties},
                 $schema->{additionalProperties},
                 $errors,
             );
@@ -1617,6 +1642,16 @@ my $schema_valid_types =
 my $default_schema_noref = {
     description => "This is the JSON Schema for JSON Schemas.",
     type => ["object"],
+    patternProperties => {
+        # Extension properties that are not part of the JSON Schema specification.
+        # This allows properties prefixed with 'x-' to be defined.
+        #
+        # Note that because such properties are not validated any further, they
+        # should be used sparingly.
+        qr/^x-/ => {
+            type => "any",
+        },
+    },
     additionalProperties => 0,
     properties => {
         type => {
@@ -1674,10 +1709,21 @@ my $default_schema_noref = {
             optional => 1,
             default => {},
         },
+        patternProperties => {
+            type => "object",
+            optional => 1,
+            description =>
+                "This provides property definitions for each property that matches the corresponding"
+                . " property name in this object. Each property name of this object should be a"
+                . " valid regular expression. Each property value of this object must be a valid"
+                . " JSON Schema.",
+        },
         additionalProperties => {
             type => ["boolean", "object"],
             description =>
-                "This provides a default property definition for all properties that are not explicitly defined in an object type definition.",
+                "This provides a default property definition for all properties that are not"
+                . " explicitly defined in an object type's 'properties' definition or match a"
+                . " pattern in 'patternProperties'.",
             optional => 1,
             default => {},
         },
@@ -1852,6 +1898,7 @@ my $default_schema = Storable::dclone($default_schema_noref);

 $default_schema->{properties}->{properties}->{additionalProperties} = $default_schema;
 $default_schema->{properties}->{additionalProperties}->{properties} = $default_schema->{properties};
+$default_schema->{properties}->{patternProperties}->{properties} = $default_schema->{properties};
 $default_schema->{properties}->{oneOf}->{items}->{properties} = $default_schema->{properties};

 $default_schema->{properties}->{items}->{properties} = $default_schema->{properties};
@@ -2452,7 +2499,7 @@ sub print_property_string {
     }

     my $errors = {};
-    check_object($path, $format, $data, undef, $errors);
+    check_object($path, $format, $data, undef, undef, $errors);
     if (scalar(%$errors)) {
         raise "format error", errors => $errors;
     }
diff --git a/test/json-schema-test.pl b/test/json-schema-test.pl
index cb3ea66..f92fedb 100755
--- a/test/json-schema-test.pl
+++ b/test/json-schema-test.pl
@@ -197,7 +197,7 @@ for my $test ($check_object_alias_tests->@*) {
             my $value = { $subtest->{in}->%* }; # shallow clone

             my $errors = {};
-            PVE::JSONSchema::check_object('test', $test->{schema}, $value, 0, $errors);
+            PVE::JSONSchema::check_object('test', $test->{schema}, $value, undef, 0, $errors);
             my $err_str = join("\n", map { "$_: $errors->{$_}" } sort keys %$errors);

             if ($subtest->{must_fail}) {
--
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH pve-storage v2 03/17] api: plugins/storage: add initial routes and endpoints
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
  2026-07-17 15:49 ` [PATCH pve-common v2 01/17] json schema: add multiline string format Max R. Carrara
  2026-07-17 15:49 ` [RFC pve-common v2 02/17] jsonschema: support 'patternProperties' and allow 'x-' keywords Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-20 12:17   ` Jakob Klocker
  2026-07-20 12:21   ` Jakob Klocker
  2026-07-17 15:49 ` [PATCH pve-storage v2 04/17] api: plugins/storage/plugin: include schema in plugin metadata Max R. Carrara
                   ` (15 subsequent siblings)
  18 siblings, 2 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Add the following routes / endpoints:

* `GET plugins/storage`
  Subdir index--lists the direct child path components (so, just
  'plugin' currently).

* `GET plugins/storage/plugin`
  Lists all installed storage plugins and their metadata.
  Currently, this is just an array of objects, with each object
  containing the plugin's `type()` and Perl module path.

* `GET plugins/storage/plugin/{type}`
  Returns the metadata for a single plugin given by `{type}`.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/API2/Makefile                  |   1 +
 src/PVE/API2/Plugins/Makefile          |  18 +++++
 src/PVE/API2/Plugins/Storage.pm        |  54 +++++++++++++
 src/PVE/API2/Plugins/Storage/Makefile  |  17 ++++
 src/PVE/API2/Plugins/Storage/Plugin.pm | 104 +++++++++++++++++++++++++
 5 files changed, 194 insertions(+)
 create mode 100644 src/PVE/API2/Plugins/Makefile
 create mode 100644 src/PVE/API2/Plugins/Storage.pm
 create mode 100644 src/PVE/API2/Plugins/Storage/Makefile
 create mode 100644 src/PVE/API2/Plugins/Storage/Plugin.pm

diff --git a/src/PVE/API2/Makefile b/src/PVE/API2/Makefile
index fe316c52..01b7b28f 100644
--- a/src/PVE/API2/Makefile
+++ b/src/PVE/API2/Makefile
@@ -5,3 +5,4 @@ install:
 	install -D -m 0644 Disks.pm ${DESTDIR}${PERLDIR}/PVE/API2/Disks.pm
 	make -C Storage install
 	make -C Disks install
+	make -C Plugins install
diff --git a/src/PVE/API2/Plugins/Makefile b/src/PVE/API2/Plugins/Makefile
new file mode 100644
index 00000000..f59e75b6
--- /dev/null
+++ b/src/PVE/API2/Plugins/Makefile
@@ -0,0 +1,18 @@
+SOURCES = Storage.pm		\
+
+
+SUBDIRS = Storage		\
+
+
+INSTALL_PATH = ${DESTDIR}${PERLDIR}/PVE/API2/Plugins
+
+
+.PHONY: install
+install:
+	set -e && for SOURCE in ${SOURCES}; \
+		do install -D -m 0644 $$SOURCE ${INSTALL_PATH}/$$SOURCE; \
+	done
+	set -e && for SUBDIR in ${SUBDIRS}; \
+		do make -C $$SUBDIR install; \
+	done
+
diff --git a/src/PVE/API2/Plugins/Storage.pm b/src/PVE/API2/Plugins/Storage.pm
new file mode 100644
index 00000000..53b883fd
--- /dev/null
+++ b/src/PVE/API2/Plugins/Storage.pm
@@ -0,0 +1,54 @@
+package PVE::API2::Plugins::Storage;
+
+use v5.36;
+
+use PVE::Storage;
+use PVE::Storage::Plugin;
+
+use PVE::API2::Plugins::Storage::Plugin;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+    subclass => 'PVE::API2::Plugins::Storage::Plugin',
+    path => 'plugin',
+});
+
+# plugins/storage
+
+__PACKAGE__->register_method({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    description => 'Directory index.',
+    permissions => {
+        user => 'all',
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {},
+    },
+    returns => {
+        type => 'array',
+        items => {
+            type => "object",
+            additionalProperties => 0,
+            properties => {
+                subdir => {
+                    type => 'string',
+                },
+            },
+        },
+        links => [{ rel => 'child', href => "{subdir}" }],
+    },
+    code => sub($param) {
+        my $result = [
+            { subdir => 'plugin' },
+        ];
+
+        return $result;
+    },
+});
+
+1;
diff --git a/src/PVE/API2/Plugins/Storage/Makefile b/src/PVE/API2/Plugins/Storage/Makefile
new file mode 100644
index 00000000..06473a2f
--- /dev/null
+++ b/src/PVE/API2/Plugins/Storage/Makefile
@@ -0,0 +1,17 @@
+SOURCES = Plugin.pm		\
+
+
+SUBDIRS =
+
+
+INSTALL_PATH = ${DESTDIR}${PERLDIR}/PVE/API2/Plugins/Storage
+
+.PHONY: install
+install:
+	set -e && for SOURCE in ${SOURCES}; \
+		do install -D -m 0644 $$SOURCE ${INSTALL_PATH}/$$SOURCE; \
+	done
+	set -e && for SUBDIR in ${SUBDIRS}; \
+		do make -C $$SUBDIR install; \
+	done
+
diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm
new file mode 100644
index 00000000..fd0f734b
--- /dev/null
+++ b/src/PVE/API2/Plugins/Storage/Plugin.pm
@@ -0,0 +1,104 @@
+package PVE::API2::Plugins::Storage::Plugin;
+
+use v5.36;
+
+use HTTP::Status qw(:constants);
+
+use PVE::Exception qw(raise);
+use PVE::Storage;
+use PVE::Storage::Plugin;
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+my $ALL_PLUGIN_TYPES = PVE::Storage::Plugin->lookup_types();
+
+my $PLUGIN_METADATA_SCHEMA = {
+    type => 'object',
+    additionalProperties => 0,
+    properties => {
+        module => {
+            type => 'string',
+            optional => 0,
+        },
+        type => {
+            type => 'string',
+            optional => 0,
+            enum => $ALL_PLUGIN_TYPES,
+        },
+    },
+};
+
+# plugins/storage/plugin
+
+__PACKAGE__->register_method({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    description => 'List all available storage plugins and their metadata.',
+    permissions => {
+        user => 'all',
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {},
+    },
+    returns => {
+        type => 'array',
+        items => $PLUGIN_METADATA_SCHEMA,
+    },
+    code => sub($param) {
+        my $result = [];
+
+        for my $type ($ALL_PLUGIN_TYPES->@*) {
+            my $plugin = PVE::Storage::Plugin->lookup($type);
+
+            my $item = {
+                module => $plugin,
+                type => $type,
+            };
+
+            push($result->@*, $item);
+        }
+
+        return $result;
+    },
+});
+
+# plugins/storage/plugin/{type}
+
+__PACKAGE__->register_method({
+    name => 'info',
+    path => '{type}',
+    method => 'GET',
+    description => 'Fetch metadata of a storage plugin.',
+    permissions => {
+        user => 'all',
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            type => {
+                type => 'string',
+            },
+        },
+    },
+    returns => $PLUGIN_METADATA_SCHEMA,
+    code => sub($param) {
+        my $param_type = extract_param($param, 'type');
+
+        my $plugin = eval { PVE::Storage::Plugin->lookup($param_type) };
+        if ($@) {
+            raise("Plugin '$param_type' not found - $@", code => HTTP_NOT_FOUND);
+        }
+
+        my $result = {
+            module => $plugin,
+            type => $param_type,
+        };
+
+        return $result;
+    },
+});
+1;
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH pve-storage v2 04/17] api: plugins/storage/plugin: include schema in plugin metadata
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (2 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH pve-storage v2 03/17] api: plugins/storage: add initial routes and endpoints Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [PATCH pve-storage v2 05/17] api: plugins/storage/plugin: mark sensitive properties in schema Max R. Carrara
                   ` (14 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Return a simple schema describing the plugin as part of its metadata.

This schema is simply a hash consisting of a given plugin's
properties' schemas. Each property schema is obtained by calling the
`get_property_schema()` method of `PVE::SectionConfig` (which
`PVE::Storage::Plugin` inherits) for each property that a given plugin
uses in its `options()`. Additionally, each plugin's `options()` are
also taken into account, and the schemas are memoized.

This means that each returned schema is completely derived from the
given plugin's section config—the (globally) available properties and
the plugin's `options()`. At the same time, this should be adaptable
enough to include extra hints, such as whether a property is sensitive
or not, for example.

Deriving a schema for each plugin like this is more preferable over
exposing `createSchema()` and `updateSchema()` of
`PVE::Storage::Plugin` directly. In particular, these two schemas come
with some drawbacks when it comes to describing an *individual*
plugin's data:

- Neither schema contains any information on which properties are used
  by which plugins.

- It is not possible to determine whether a property is actually
  optional (or not) for a given plugin. If one plugin declares a
  property as optional somewhere, the property will be optional in the
  schema in most cases, despite being non-optional for other plugins.
  (This is generally true; skipping over a bunch of SectionConfig
  implementation details here for the sake of brevity).

- The same is true even more so for fixed properties. While the
  `updateSchema()` will oftentimes *not* contain fixed properties,
  meaning that you can compare it with `createSchema()` and figure out
  which properties *may* be fixed, this only holds if the property is
  fixed in *every* plugin's `options()`. As soon as a plugin declares
  a usually fixed property as non-fixed, the property is included in
  the `updateSchema()`.

- Even when switching to property isolation for
  `PVE::Storage::Plugin`, which properties are fixed / optional for
  individual plugins is still not consistently determinable for the
  previous two reasons.

This means that in addition to exposing `createSchema()` and
`updateSchema()`, we would also have to expose the `options()` of each
plugin, and then stitch all of that information together again just to
obtain the hash that the `get_schema_for_plugin()` helper being added
here returns.

Additionally, `get_property_schema()` takes property isolation into
account. More precisely, if one were to enable property isolation and
copy-paste the formerly global property definitions into the
`properties()` of each plugin where needed, the returned schema would
stay the same.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/API2/Plugins/Storage/Plugin.pm | 34 ++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm
index fd0f734b..573d3e4d 100644
--- a/src/PVE/API2/Plugins/Storage/Plugin.pm
+++ b/src/PVE/API2/Plugins/Storage/Plugin.pm
@@ -22,6 +22,10 @@ my $PLUGIN_METADATA_SCHEMA = {
             type => 'string',
             optional => 0,
         },
+        schema => {
+            type => 'object',
+            optional => 0,
+        },
         type => {
             type => 'string',
             optional => 0,
@@ -30,6 +34,34 @@ my $PLUGIN_METADATA_SCHEMA = {
     },
 };
 
+my $PLUGIN_SCHEMAS = {};
+
+my sub get_schema_for_plugin : prototype($) ($plugin) {
+    my $type = $plugin->type();
+
+    return $PLUGIN_SCHEMAS->{$type} if defined($PLUGIN_SCHEMAS->{$type});
+
+    my $options = $plugin->options();
+
+    my $schema = {};
+    $PLUGIN_SCHEMAS->{$type} = $schema;
+
+    for my $option (keys $options->%*) {
+        my $prop_schema = PVE::RESTHandler::api_dump_remove_refs(
+            PVE::Storage::Plugin->get_property_schema($type, $option));
+
+        # shallow copy
+        my $property = { $prop_schema->%* };
+        $schema->{$option} = $property;
+
+        for my $opt_key (keys $options->{$option}->%*) {
+            $property->{$opt_key} = $options->{$option}->{$opt_key};
+        }
+    }
+
+    return $schema;
+}
+
 # plugins/storage/plugin
 
 __PACKAGE__->register_method({
@@ -56,6 +88,7 @@ __PACKAGE__->register_method({
 
             my $item = {
                 module => $plugin,
+                schema => get_schema_for_plugin($plugin),
                 type => $type,
             };
 
@@ -95,6 +128,7 @@ __PACKAGE__->register_method({
 
         my $result = {
             module => $plugin,
+            schema => get_schema_for_plugin($plugin),
             type => $param_type,
         };
 
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH pve-storage v2 05/17] api: plugins/storage/plugin: mark sensitive properties in schema
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (3 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH pve-storage v2 04/17] api: plugins/storage/plugin: include schema in plugin metadata Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [PATCH pve-storage v2 06/17] api: plugins/storage/plugin: factor plugin metadata code into helper Max R. Carrara
                   ` (13 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/API2/Plugins/Storage/Plugin.pm | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm
index 573d3e4d..476a9546 100644
--- a/src/PVE/API2/Plugins/Storage/Plugin.pm
+++ b/src/PVE/API2/Plugins/Storage/Plugin.pm
@@ -42,6 +42,7 @@ my sub get_schema_for_plugin : prototype($) ($plugin) {
     return $PLUGIN_SCHEMAS->{$type} if defined($PLUGIN_SCHEMAS->{$type});
 
     my $options = $plugin->options();
+    my $plugindata = $plugin->plugindata();
 
     my $schema = {};
     $PLUGIN_SCHEMAS->{$type} = $schema;
@@ -52,6 +53,8 @@ my sub get_schema_for_plugin : prototype($) ($plugin) {
 
         # shallow copy
         my $property = { $prop_schema->%* };
+        $property->{sensitive} = defined($plugindata->{'sensitive-properties'}->{$option}) ? 1 : 0;
+
         $schema->{$option} = $property;
 
         for my $opt_key (keys $options->{$option}->%*) {
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH pve-storage v2 06/17] api: plugins/storage/plugin: factor plugin metadata code into helper
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (4 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH pve-storage v2 05/17] api: plugins/storage/plugin: mark sensitive properties in schema Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [PATCH pve-storage v2 07/17] api: plugins/storage/plugin: add plugins' 'content' to their metadata Max R. Carrara
                   ` (12 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

in order to reduce the number of places that need to be touched when
adding more properties to the returned metadata.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/API2/Plugins/Storage/Plugin.pm | 35 +++++++++++---------------
 1 file changed, 15 insertions(+), 20 deletions(-)

diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm
index 476a9546..2b4970bd 100644
--- a/src/PVE/API2/Plugins/Storage/Plugin.pm
+++ b/src/PVE/API2/Plugins/Storage/Plugin.pm
@@ -65,6 +65,19 @@ my sub get_schema_for_plugin : prototype($) ($plugin) {
     return $schema;
 }
 
+my sub build_plugin_metadata : prototype($) ($type) {
+    my $plugin = eval { PVE::Storage::Plugin->lookup($type) };
+    if ($@) {
+        raise("Plugin '$type' not found - $@", code => HTTP_NOT_FOUND);
+    }
+
+    return {
+        module => $plugin,
+        schema => get_schema_for_plugin($plugin),
+        type => $type,
+    };
+}
+
 # plugins/storage/plugin
 
 __PACKAGE__->register_method({
@@ -87,14 +100,7 @@ __PACKAGE__->register_method({
         my $result = [];
 
         for my $type ($ALL_PLUGIN_TYPES->@*) {
-            my $plugin = PVE::Storage::Plugin->lookup($type);
-
-            my $item = {
-                module => $plugin,
-                schema => get_schema_for_plugin($plugin),
-                type => $type,
-            };
-
+            my $item = build_plugin_metadata($type);
             push($result->@*, $item);
         }
 
@@ -124,18 +130,7 @@ __PACKAGE__->register_method({
     code => sub($param) {
         my $param_type = extract_param($param, 'type');
 
-        my $plugin = eval { PVE::Storage::Plugin->lookup($param_type) };
-        if ($@) {
-            raise("Plugin '$param_type' not found - $@", code => HTTP_NOT_FOUND);
-        }
-
-        my $result = {
-            module => $plugin,
-            schema => get_schema_for_plugin($plugin),
-            type => $param_type,
-        };
-
-        return $result;
+        return build_plugin_metadata($param_type);
     },
 });
 1;
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH pve-storage v2 07/17] api: plugins/storage/plugin: add plugins' 'content' to their metadata
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (5 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH pve-storage v2 06/17] api: plugins/storage/plugin: factor plugin metadata code into helper Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [PATCH pve-storage v2 08/17] all plugins: add 'title' to properties, adapt 'description's Max R. Carrara
                   ` (11 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/API2/Plugins/Storage/Plugin.pm | 27 ++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm
index 2b4970bd..120aef44 100644
--- a/src/PVE/API2/Plugins/Storage/Plugin.pm
+++ b/src/PVE/API2/Plugins/Storage/Plugin.pm
@@ -18,6 +18,27 @@ my $PLUGIN_METADATA_SCHEMA = {
     type => 'object',
     additionalProperties => 0,
     properties => {
+        content => {
+            type => 'object',
+            optional => 0,
+            properties => {
+                supported => {
+                    type => 'array',
+                    optional => 0,
+                    items => {
+                        type => 'string',
+                    },
+                },
+                default => {
+                    type => 'array',
+                    optional => 0,
+                    items => {
+                        type => 'string',
+                    },
+                },
+            },
+            additionalProperties => 0,
+        },
         module => {
             type => 'string',
             optional => 0,
@@ -71,7 +92,13 @@ my sub build_plugin_metadata : prototype($) ($type) {
         raise("Plugin '$type' not found - $@", code => HTTP_NOT_FOUND);
     }
 
+    my $plugindata = $plugin->plugindata();
+
     return {
+        content => {
+            supported => [sort keys $plugindata->{content}->[0]->%*],
+            default => [sort keys $plugindata->{content}->[1]->%*],
+        },
         module => $plugin,
         schema => get_schema_for_plugin($plugin),
         type => $type,
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH pve-storage v2 08/17] all plugins: add 'title' to properties, adapt 'description's
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (6 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH pve-storage v2 07/17] api: plugins/storage/plugin: add plugins' 'content' to their metadata Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [RFC pve-storage v2 09/17] plugin: mark 'preallocation' and 'content-dirs' properties as advanced Max R. Carrara
                   ` (10 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Add a `title` [1] to almost all plugins' properties. Since this
keyword is included when querying the `plugins/storage/plugin`
endpoint, it can be used for cosmetic purposes, such as field labels
in the UI or similar.

The only properties that do not get a `title` are `mkdir` and
`authsupported`; the former is deprecated and the latter is currently
unused.

If a property is exposed in the UI, the label of its corresponding
field is used as its `title`, with minor alterations at most (e.g.
spelling out "Filesystem" instead of "FS"). Otherwise, something
sensible and simple is chosen instead.

Additionally, adapt some `description` [1] keywords because they are
either very short, not general enough (if used by multiple plugins),
or not really descriptive. In certain cases the wording is improved a
little.

Notably, fix the line-breaking of the descriptions of the fields in
the `$prune_backups_format` hash by adding a space at the beginning of
the concatenated strings.

There is still room for improvement here, especially since some
properties are kind of awkward to describe in more general terms, so
this is nothing that should be considered set in stone.

All of this is done as preparation for supporting custom (third-party)
storage plugins in the web UI.

Note that altering the `description` keywords affects the descriptions
of the flags in the `pvesm` manpage, in particular for the `pvesm add`
and `pvesm set` subcommands, and also possibly other parts of our
documentation.

[1]: https://json-schema.org/draft/2020-12/json-schema-validation#name-title-and-description

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/Storage/BTRFSPlugin.pm   |  1 +
 src/PVE/Storage/CIFSPlugin.pm    | 10 ++++---
 src/PVE/Storage/CephFSPlugin.pm  |  6 +++--
 src/PVE/Storage/DirPlugin.pm     | 11 +++++---
 src/PVE/Storage/ESXiPlugin.pm    |  1 +
 src/PVE/Storage/ISCSIPlugin.pm   |  6 +++--
 src/PVE/Storage/LVMPlugin.pm     | 10 +++++--
 src/PVE/Storage/LvmThinPlugin.pm |  3 ++-
 src/PVE/Storage/NFSPlugin.pm     |  6 +++--
 src/PVE/Storage/PBSPlugin.pm     |  5 +++-
 src/PVE/Storage/Plugin.pm        | 46 +++++++++++++++++++++++++-------
 src/PVE/Storage/RBDPlugin.pm     | 16 ++++++++---
 src/PVE/Storage/ZFSPlugin.pm     | 16 +++++++----
 src/PVE/Storage/ZFSPoolPlugin.pm |  9 ++++---
 14 files changed, 109 insertions(+), 37 deletions(-)

diff --git a/src/PVE/Storage/BTRFSPlugin.pm b/src/PVE/Storage/BTRFSPlugin.pm
index fb47aa0b..6b2e09ad 100644
--- a/src/PVE/Storage/BTRFSPlugin.pm
+++ b/src/PVE/Storage/BTRFSPlugin.pm
@@ -52,6 +52,7 @@ sub plugindata {
 sub properties {
     return {
         nocow => {
+            title => "Set NOCOW Flag",
             description => "Set the NOCOW flag on files."
                 . " Disables data checksumming and causes data errors to be unrecoverable from"
                 . " while allowing direct I/O. Only use this if data does not need to be any more"
diff --git a/src/PVE/Storage/CIFSPlugin.pm b/src/PVE/Storage/CIFSPlugin.pm
index 54f0f4ec..23b505cd 100644
--- a/src/PVE/Storage/CIFSPlugin.pm
+++ b/src/PVE/Storage/CIFSPlugin.pm
@@ -132,21 +132,25 @@ sub plugindata {
 sub properties {
     return {
         share => {
-            description => "CIFS share.",
+            title => "Share",
+            description => "The name of the share to use.",
             type => 'string',
         },
         password => {
-            description => "Password for accessing the share/datastore.",
+            title => "Password",
+            description => "The password used for authentication.",
             type => 'string',
             maxLength => 256,
         },
         domain => {
-            description => "CIFS domain.",
+            title => "Domain",
+            description => "The name of the user domain (workgroup) to use.",
             type => 'string',
             optional => 1,
             maxLength => 256,
         },
         smbversion => {
+            title => "SMB Version",
             description =>
                 "SMB protocol version. 'default' if not set, negotiates the highest SMB2+"
                 . " version supported by both the client and server.",
diff --git a/src/PVE/Storage/CephFSPlugin.pm b/src/PVE/Storage/CephFSPlugin.pm
index fbc97113..fcb8a9e6 100644
--- a/src/PVE/Storage/CephFSPlugin.pm
+++ b/src/PVE/Storage/CephFSPlugin.pm
@@ -125,11 +125,13 @@ sub plugindata {
 sub properties {
     return {
         fuse => {
-            description => "Mount CephFS through FUSE.",
+            title => "Mount with FUSE",
+            description => "Mount the filesystem through FUSE.",
             type => 'boolean',
         },
         'fs-name' => {
-            description => "The Ceph filesystem name.",
+            title => "Filesystem Name",
+            description => "The name or identifier of the filesystem.",
             type => 'string',
             format => 'pve-configid',
         },
diff --git a/src/PVE/Storage/DirPlugin.pm b/src/PVE/Storage/DirPlugin.pm
index 80c4a031..cc52feae 100644
--- a/src/PVE/Storage/DirPlugin.pm
+++ b/src/PVE/Storage/DirPlugin.pm
@@ -45,7 +45,9 @@ sub plugindata {
 sub properties {
     return {
         path => {
-            description => "File system path.",
+            title => "Path",
+            description => "The path on the filesystem that leads to the"
+                . " mountpoint or base directory of the storage.",
             type => 'string',
             format => 'pve-storage-path',
         },
@@ -57,23 +59,26 @@ sub properties {
             default => 'yes',
         },
         'create-base-path' => {
+            title => "Create Base Directory",
             description => "Create the base directory if it doesn't exist.",
             type => 'boolean',
             default => 'yes',
         },
         'create-subdirs' => {
-            description => "Populate the directory with the default structure.",
+            title => "Populate Base Directory",
+            description => "Populate the base directory with the default structure.",
             type => 'boolean',
             default => 'yes',
         },
         is_mountpoint => {
+            title => "Is Mountpoint",
             description => "Assume the given path is an externally managed mountpoint "
                 . "and consider the storage offline if it is not mounted. "
                 . "Using a boolean (yes/no) value serves as a shortcut to using the target path in this field.",
             type => 'string',
             default => 'no',
         },
-        bwlimit => get_standard_option('bwlimit'),
+        bwlimit => get_standard_option('bwlimit', { title => "Bandwidth Limit" }),
     };
 }
 
diff --git a/src/PVE/Storage/ESXiPlugin.pm b/src/PVE/Storage/ESXiPlugin.pm
index 19f23bb9..b5e4135e 100644
--- a/src/PVE/Storage/ESXiPlugin.pm
+++ b/src/PVE/Storage/ESXiPlugin.pm
@@ -38,6 +38,7 @@ sub plugindata {
 sub properties {
     return {
         'skip-cert-verification' => {
+            title => "Skip Certificate Verification",
             description =>
                 'Disable TLS certificate verification, only enable on fully trusted networks!',
             type => 'boolean',
diff --git a/src/PVE/Storage/ISCSIPlugin.pm b/src/PVE/Storage/ISCSIPlugin.pm
index 801b5d1b..a35a9882 100644
--- a/src/PVE/Storage/ISCSIPlugin.pm
+++ b/src/PVE/Storage/ISCSIPlugin.pm
@@ -341,11 +341,13 @@ sub plugindata {
 sub properties {
     return {
         target => {
-            description => "iSCSI target.",
+            title => "Target",
+            description => "iSCSI target name (an iSCSI Qualified Name - IQN).",
             type => 'string',
         },
         portal => {
-            description => "iSCSI portal (IP or DNS name with optional port).",
+            title => "Portal",
+            description => "iSCSI portal (IP or hostname, optionally with port).",
             type => 'string',
             format => 'pve-storage-portal-dns',
         },
diff --git a/src/PVE/Storage/LVMPlugin.pm b/src/PVE/Storage/LVMPlugin.pm
index a313ecc8..903eda3a 100644
--- a/src/PVE/Storage/LVMPlugin.pm
+++ b/src/PVE/Storage/LVMPlugin.pm
@@ -415,20 +415,24 @@ sub plugindata {
 sub properties {
     return {
         vgname => {
-            description => "Volume group name.",
+            title => "Volume Group",
+            description => "LVM volume group name.",
             type => 'string',
             format => 'pve-storage-vgname',
         },
         base => {
-            description => "Base volume. This volume is automatically activated.",
+            title => "Base Storage",
+            description => "Name of the base volume (automatically activated).",
             type => 'string',
             format => 'pve-volume-id',
         },
         saferemove => {
+            title => "Wipe Removed Volumes",
             description => "Zero-out data when removing LVs.",
             type => 'boolean',
         },
         'saferemove-stepsize' => {
+            title => "Wipe Step Size",
             description => "Wipe step size in MiB."
                 . " It will be capped to the maximum supported by the storage.",
             default => 32,
@@ -436,10 +440,12 @@ sub properties {
             type => 'integer',
         },
         saferemove_throughput => {
+            title => "Wipe Throughput",
             description => "Wipe throughput (cstream -t parameter value).",
             type => 'string',
         },
         tagged_only => {
+            title => "Tagged Volumes Only",
             description => "Only list logical volumes tagged with 'pve-vm-ID'.",
             type => 'boolean',
         },
diff --git a/src/PVE/Storage/LvmThinPlugin.pm b/src/PVE/Storage/LvmThinPlugin.pm
index cadf343f..0f3b21d4 100644
--- a/src/PVE/Storage/LvmThinPlugin.pm
+++ b/src/PVE/Storage/LvmThinPlugin.pm
@@ -39,7 +39,8 @@ sub plugindata {
 sub properties {
     return {
         thinpool => {
-            description => "LVM thin pool LV name.",
+            title => "Thin Pool",
+            description => "Name of the LVM thin pool logical volume.",
             type => 'string',
             format => 'pve-storage-vgname',
         },
diff --git a/src/PVE/Storage/NFSPlugin.pm b/src/PVE/Storage/NFSPlugin.pm
index 4cc02c9e..e54c4a4a 100644
--- a/src/PVE/Storage/NFSPlugin.pm
+++ b/src/PVE/Storage/NFSPlugin.pm
@@ -73,12 +73,14 @@ sub plugindata {
 sub properties {
     return {
         export => {
-            description => "NFS export path.",
+            title => "Export Path",
+            description => "Path exported by the NFS server (e.g., /srv/nfs).",
             type => 'string',
             format => 'pve-storage-path',
         },
         server => {
-            description => "Server IP or DNS name.",
+            title => "Server",
+            description => "IP address or hostname of the server.",
             type => 'string',
             format => 'pve-storage-server',
         },
diff --git a/src/PVE/Storage/PBSPlugin.pm b/src/PVE/Storage/PBSPlugin.pm
index 6b049b47..eb907aa3 100644
--- a/src/PVE/Storage/PBSPlugin.pm
+++ b/src/PVE/Storage/PBSPlugin.pm
@@ -42,17 +42,20 @@ sub plugindata {
 sub properties {
     return {
         datastore => {
+            title => "Datastore",
             description => "Proxmox Backup Server datastore name.",
             type => 'string',
         },
         # openssl s_client -connect <host>:8007 2>&1 |openssl x509 -fingerprint -sha256
-        fingerprint => get_standard_option('fingerprint-sha256'),
+        fingerprint => get_standard_option('fingerprint-sha256', { title => "Fingerprint" }),
         'encryption-key' => {
+            title => "Encryption Key",
             description =>
                 "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
             type => 'string',
         },
         'master-pubkey' => {
+            title => "Master Public Key",
             description =>
                 "Base64-encoded, PEM-formatted public RSA key. Used to encrypt a copy of the encryption-key which will be added to each encrypted backup.",
             type => 'string',
diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index 4f69f9b5..d8f6987a 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -88,36 +88,44 @@ my %prune_option = (
 our $prune_backups_format = {
     'keep-all' => {
         type => 'boolean',
+        title => "Keep All Backups",
         description => 'Keep all backups. Conflicts with the other options when true.',
         optional => 1,
     },
     'keep-last' => {
-        %prune_option, description => 'Keep the last <N> backups.',
+        %prune_option,
+        title => "Keep Last",
+        description => 'Keep the last <N> backups.',
     },
     'keep-hourly' => {
         %prune_option,
+        title => "Keep Hourly",
         description => 'Keep backups for the last <N> different hours. If there is more'
-            . 'than one backup for a single hour, only the latest one is kept.',
+            . ' than one backup for a single hour, only the latest one is kept.',
     },
     'keep-daily' => {
         %prune_option,
+        title => "Keep Daily",
         description => 'Keep backups for the last <N> different days. If there is more'
-            . 'than one backup for a single day, only the latest one is kept.',
+            . ' than one backup for a single day, only the latest one is kept.',
     },
     'keep-weekly' => {
         %prune_option,
+        title => "Keep Weekly",
         description => 'Keep backups for the last <N> different weeks. If there is more'
-            . 'than one backup for a single week, only the latest one is kept.',
+            . ' than one backup for a single week, only the latest one is kept.',
     },
     'keep-monthly' => {
         %prune_option,
+        title => "Keep Monthly",
         description => 'Keep backups for the last <N> different months. If there is more'
-            . 'than one backup for a single month, only the latest one is kept.',
+            . ' than one backup for a single month, only the latest one is kept.',
     },
     'keep-yearly' => {
         %prune_option,
+        title => "Keep Yearly",
         description => 'Keep backups for the last <N> different years. If there is more'
-            . 'than one backup for a single year, only the latest one is kept.',
+            . ' than one backup for a single year, only the latest one is kept.',
     },
 };
 PVE::JSONSchema::register_format('prune-backups', $prune_backups_format, \&validate_prune_backups);
@@ -140,6 +148,7 @@ sub validate_prune_backups {
 register_standard_option(
     'prune-backups',
     {
+        title => "Backup Retention",
         description => "The retention options with shorter intervals are processed first "
             . "with --keep-last being the very first one. Each option covers a "
             . "specific period of time. We say that backups within this period "
@@ -153,19 +162,27 @@ register_standard_option(
 
 my $defaultData = {
     propertyList => {
-        type => { description => "Storage type." },
+        type => {
+            title => "Type",
+            description => "Storage type.",
+        },
         storage => get_standard_option(
             'pve-storage-id',
-            { completion => \&PVE::Storage::complete_storage },
+            {
+                title => "ID",
+                completion => \&PVE::Storage::complete_storage,
+            },
         ),
         nodes => get_standard_option(
             'pve-node-list',
             {
+                title => "Nodes",
                 description => "List of nodes for which the storage configuration applies.",
                 optional => 1,
             },
         ),
         content => {
+            title => "Content",
             description => "Allowed content types.\n\nNOTE: the value "
                 . "'rootdir' is used for Containers, and value 'images' for VMs.\n",
             type => 'string',
@@ -174,12 +191,14 @@ my $defaultData = {
             completion => \&PVE::Storage::complete_content_type,
         },
         disable => {
+            title => "Disable",
             description => "Flag to disable the storage.",
             type => 'boolean',
             optional => 1,
         },
         'prune-backups' => get_standard_option('prune-backups'),
         'max-protected-backups' => {
+            title => "Maximum Protected",
             description =>
                 "Maximal number of protected backups per guest. Use '-1' for unlimited.",
             type => 'integer',
@@ -189,6 +208,7 @@ my $defaultData = {
                 "Unlimited for users with Datastore.Allocate privilege, 5 for other users",
         },
         shared => {
+            title => "Shared",
             description =>
                 "Indicate that this is a single storage with the same contents on all "
                 . "nodes (or all listed in the 'nodes' option). It will not make the contents of a "
@@ -198,6 +218,7 @@ my $defaultData = {
             optional => 1,
         },
         subdir => {
+            title => "Subdirectory",
             description => "Subdir to mount.",
             type => 'string',
             format => 'pve-storage-path',
@@ -206,11 +227,13 @@ my $defaultData = {
         format => get_standard_option(
             'pve-storage-image-format',
             {
+                title => "Default Format",
                 description => "Default image format.",
                 optional => 1,
             },
         ),
         preallocation => {
+            title => "Preallocation",
             description => "Preallocation mode for raw and qcow2 images. "
                 . "Using 'metadata' on raw images results in preallocation=off.",
             type => 'string',
@@ -219,18 +242,22 @@ my $defaultData = {
             optional => 1,
         },
         'content-dirs' => {
+            title => "Content Directories",
             description => "Overrides for default content type directories.",
             type => "string",
             format => "pve-dir-override-list",
             optional => 1,
         },
         options => {
-            description => "NFS/CIFS mount options (see 'man nfs' or 'man mount.cifs')",
+            title => "Mount Options",
+            description => "A list of comma-separated mount options for the filesystem."
+                . " See 'man 5 fstab' and 'man 8 mount' for more information.",
             type => 'string',
             format => 'pve-storage-options',
             optional => 1,
         },
         port => {
+            title => "Port",
             description =>
                 "Use this port to connect to the storage instead of the default one (for"
                 . " example, with PBS or ESXi). For NFS and CIFS, use the 'options' option to"
@@ -242,6 +269,7 @@ my $defaultData = {
         },
         'snapshot-as-volume-chain' => {
             type => 'boolean',
+            title => "Allow Snapshots as Volume-Chain",
             description => 'Enable support for creating storage-vendor agnostic snapshot'
                 . ' through volume backing-chains.',
             default => 0,
diff --git a/src/PVE/Storage/RBDPlugin.pm b/src/PVE/Storage/RBDPlugin.pm
index b5374251..bf05f739 100644
--- a/src/PVE/Storage/RBDPlugin.pm
+++ b/src/PVE/Storage/RBDPlugin.pm
@@ -419,24 +419,30 @@ sub plugindata {
 sub properties {
     return {
         monhost => {
+            title => "Monitor(s)",
             description => "IP addresses of monitors (for external clusters).",
             type => 'string',
             format => 'pve-storage-portal-dns-list',
         },
         pool => {
-            description => "Pool.",
+            title => "Pool",
+            description => "Name of the pool that stores images.",
             type => 'string',
         },
         'data-pool' => {
-            description => "Data Pool (for erasure coding only)",
+            title => "Data Pool",
+            description => "Data pool name for erasure-coded images.",
             type => 'string',
         },
         namespace => {
-            description => "Namespace.",
+            title => "Namespace",
+            description =>
+                "The namespace used for grouping, isolation, or partitioning purposes.",
             type => 'string',
         },
         username => {
-            description => "RBD Id.",
+            title => "Username",
+            description => "The username used for authentication.",
             type => 'string',
         },
         authsupported => {
@@ -444,11 +450,13 @@ sub properties {
             type => 'string',
         },
         krbd => {
+            title => "KRBD",
             description => "Always access rbd through krbd kernel module.",
             type => 'boolean',
             default => 0,
         },
         keyring => {
+            title => "Keyring",
             description => "Client keyring contents (for external clusters).",
             type => 'string',
         },
diff --git a/src/PVE/Storage/ZFSPlugin.pm b/src/PVE/Storage/ZFSPlugin.pm
index 74e0a084..7fef9303 100644
--- a/src/PVE/Storage/ZFSPlugin.pm
+++ b/src/PVE/Storage/ZFSPlugin.pm
@@ -183,29 +183,35 @@ sub plugindata {
 sub properties {
     return {
         iscsiprovider => {
-            description => "iscsi provider",
+            title => "iSCSI Provider",
+            description => "The iSCSI target implementation used on the remote machine.",
             type => 'string',
         },
         # this will disable write caching on comstar and istgt.
         # it is not implemented for iet. iet blockio always operates with
         # writethrough caching when not in readonly mode
         nowritecache => {
-            description => "disable write caching on the target",
+            title => "Disable Write Cache",
+            description => "Disable write caching on the iSCSI target.",
             type => 'boolean',
         },
         comstar_tg => {
-            description => "target group for comstar views",
+            title => "Target Group",
+            description => "Target group for Comstar views.",
             type => 'string',
         },
         comstar_hg => {
-            description => "host group for comstar views",
+            title => "Host Group",
+            description => "Host group for Comstar views.",
             type => 'string',
         },
         lio_tpg => {
-            description => "target portal group for Linux LIO targets",
+            title => "Target Portal Group",
+            description => "Target portal group for Linux LIO targets.",
             type => 'string',
         },
         'zfs-base-path' => {
+            title => "Base Path",
             description => "Base path where to look for the created ZFS block devices. Set"
                 . " automatically during creation if not specified. Usually '/dev/zvol'.",
             type => 'string',
diff --git a/src/PVE/Storage/ZFSPoolPlugin.pm b/src/PVE/Storage/ZFSPoolPlugin.pm
index 86307449..dd349f50 100644
--- a/src/PVE/Storage/ZFSPoolPlugin.pm
+++ b/src/PVE/Storage/ZFSPoolPlugin.pm
@@ -63,17 +63,20 @@ sub plugindata {
 sub properties {
     return {
         blocksize => {
-            description => "ZFS block size",
+            title => "Block Size",
+            description => "ZFS block size.",
             type => 'string',
             format => 'pve-storage-zfs-blocksize',
             format_description => 'a power of 2 with optional k or m suffix',
         },
         sparse => {
-            description => "use sparse volumes",
+            title => "Thin Provision",
+            description => "Use sparse (thin-provisioned) volumes.",
             type => 'boolean',
         },
         mountpoint => {
-            description => "mount point",
+            title => "Mountpoint",
+            description => "Path to the directory where the storage is mounted.",
             type => 'string',
             format => 'pve-storage-path',
         },
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [RFC pve-storage v2 09/17] plugin: mark 'preallocation' and 'content-dirs' properties as advanced
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (7 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH pve-storage v2 08/17] all plugins: add 'title' to properties, adapt 'description's Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [RFC pve-storage v2 10/17] plugin, dirplugin: mark certain properties as hidden Max R. Carrara
                   ` (9 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Add a new 'x-advanced' keyword to the 'preallocation' and
'content-dirs' properties. This keyword will act as hint in the UI to
place the fields corresponding to these properties in the "Advanced"
section of the storage config create / edit panel.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
NOTE: This patch is experimental and thus marked as RFC.

 src/PVE/Storage/Plugin.pm | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index d8f6987a..a9776240 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -240,6 +240,7 @@ my $defaultData = {
             enum => ['off', 'metadata', 'falloc', 'full'],
             default => 'metadata',
             optional => 1,
+            'x-advanced' => 1,
         },
         'content-dirs' => {
             title => "Content Directories",
@@ -274,6 +275,7 @@ my $defaultData = {
                 . ' through volume backing-chains.',
             default => 0,
             optional => 1,
+            'x-advanced' => 1,
         },
     },
 };
--
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [RFC pve-storage v2 10/17] plugin, dirplugin: mark certain properties as hidden
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (8 preceding siblings ...)
  2026-07-17 15:49 ` [RFC pve-storage v2 09/17] plugin: mark 'preallocation' and 'content-dirs' properties as advanced Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [PATCH proxmox-widget-toolkit v2 11/17] form: introduce new 'proxmoxtextarea' field Max R. Carrara
                   ` (8 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Add a new 'x-hidden' keyword to the 'bwlimit', 'create-base-path',
'content-dirs', 'create-subdirs' and 'format' properties. This keyword
will act as a hint in the UI to not include the fields corresponding
to these properties in the storage config create / edit panel at all.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
NOTE: This patch is experimental and thus marked as RFC.

 src/PVE/Storage/DirPlugin.pm | 10 +++++++++-
 src/PVE/Storage/Plugin.pm    |  2 ++
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/PVE/Storage/DirPlugin.pm b/src/PVE/Storage/DirPlugin.pm
index cc52feae..9c98ebed 100644
--- a/src/PVE/Storage/DirPlugin.pm
+++ b/src/PVE/Storage/DirPlugin.pm
@@ -63,12 +63,14 @@ sub properties {
             description => "Create the base directory if it doesn't exist.",
             type => 'boolean',
             default => 'yes',
+            'x-hidden' => 1,
         },
         'create-subdirs' => {
             title => "Populate Base Directory",
             description => "Populate the base directory with the default structure.",
             type => 'boolean',
             default => 'yes',
+            'x-hidden' => 1,
         },
         is_mountpoint => {
             title => "Is Mountpoint",
@@ -78,7 +80,13 @@ sub properties {
             type => 'string',
             default => 'no',
         },
-        bwlimit => get_standard_option('bwlimit', { title => "Bandwidth Limit" }),
+        bwlimit => get_standard_option(
+            'bwlimit',
+            {
+                title => "Bandwidth Limit",
+                'x-hidden' => 1,
+            },
+        ),
     };
 }

diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index a9776240..c5fab9ef 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -230,6 +230,7 @@ my $defaultData = {
                 title => "Default Format",
                 description => "Default image format.",
                 optional => 1,
+                'x-hidden' => 1,
             },
         ),
         preallocation => {
@@ -248,6 +249,7 @@ my $defaultData = {
             type => "string",
             format => "pve-dir-override-list",
             optional => 1,
+            'x-hidden' => 1,
         },
         options => {
             title => "Mount Options",
--
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH proxmox-widget-toolkit v2 11/17] form: introduce new 'proxmoxtextarea' field
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (9 preceding siblings ...)
  2026-07-17 15:49 ` [RFC pve-storage v2 10/17] plugin, dirplugin: mark certain properties as hidden Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [PATCH proxmox-widget-toolkit v2 12/17] utils: introduce helper function getFieldDefFromPropertySchema Max R. Carrara
                   ` (7 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Define a new field named `proxmoxtextarea` and let it extend the
`TextArea` field that Ext JS ships.

`proxmoxtextarea` uses the exact same logic as `proxmoxtextfield`.
This means that it also supports the `skipEmptyText` attribute, which
is used to omit sending an empty string to the backend if the field is
left empty.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/form/TextAreaField.js | 47 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 47 insertions(+)

diff --git a/src/form/TextAreaField.js b/src/form/TextAreaField.js
index f4b0bb4..ae2e922 100644
--- a/src/form/TextAreaField.js
+++ b/src/form/TextAreaField.js
@@ -59,3 +59,50 @@ Ext.define('Proxmox.form.field.Base64TextArea', {
         this.validate();
     },
 });
+
+Ext.define('Proxmox.form.field.TextArea', {
+    extend: 'Ext.form.field.TextArea',
+    alias: ['widget.proxmoxtextarea'],
+
+    config: {
+        skipEmptyText: true,
+        deleteEmpty: false,
+        trimValue: false,
+    },
+
+    getSubmitData: function () {
+        let me = this,
+            data = null,
+            val;
+        if (!me.disabled && me.submitValue && !me.isFileUpload()) {
+            val = me.getSubmitValue();
+            if (val !== null) {
+                data = {};
+                data[me.getName()] = val;
+            } else if (me.getDeleteEmpty()) {
+                data = {};
+                data.delete = me.getName();
+            }
+        }
+        return data;
+    },
+
+    getSubmitValue: function () {
+        let me = this;
+
+        let value = this.processRawValue(this.getRawValue());
+        if (me.getTrimValue() && typeof value === 'string') {
+            value = value.trim();
+        }
+        if (value !== '') {
+            return value;
+        }
+
+        return me.getSkipEmptyText() ? null : value;
+    },
+
+    setAllowBlank: function (allowBlank) {
+        this.allowBlank = allowBlank;
+        this.validate();
+    },
+});
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH proxmox-widget-toolkit v2 12/17] utils: introduce helper function getFieldDefFromPropertySchema
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (10 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH proxmox-widget-toolkit v2 11/17] form: introduce new 'proxmoxtextarea' field Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [PATCH proxmox-widget-toolkit v2 13/17] acme: use helper to construct ExtJS fields from property schemas Max R. Carrara
                   ` (6 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

This helper takes the JSON schema of a single property and transforms
it into an object representing an Ext JS field.

Currently, four types for properties are supported: `string`,
`integer`, `number` and `boolean`. Many JSON schema validation
keywords are also supported and converted to Ext JS analogues.

Types --> Ext JS field types:
- boolean --> proxmoxcheckbox
- integer --> proxmoxintegerfield
- number  --> numberfield
- string  --> proxmoxtextfield

Keywords --> Ext JS properties / behavior:
- minLength   --> minLength
- maxLength   --> maxLength
- minimum     --> minValue
- maximum     --> maxValue
- title       --> fieldLabel (using the field's name if no title)
- optional    --> allowBlank
- description --> shown on hover
- enum        --> use proxmoxKVComboBox as field instead
- default     --> set as `value` of the field and use as `emptyText`

Furthermore, add support for the `multiline` JSON schema `format`.
This means that any property with the `multiline` format becomes a
`proxmoxtextarea` field.

Additionally, the helper also takes "extra attributes" and a "context"
which allows for more fine-grained control over how the ExtJS field is
actually constructed and also makes it possible to express different
UI-specific behaviors that cannot be modeled via JSONSchema.

The only variable in the context is `isCreate`, which is used to hint
whether the field is used when creating a record or not.

The following extra attributes are added:

- sensitive

  Marks a field as "sensitive", which is useful for password fields
  for example. This causes most fields to become `proxmoxtextfield`s
  instead, with their inputs obfuscated.

  Fields with the `multiline` format are kept as `proxmoxtextarea` to
  allow copy-pasting private keys into the field or similar.

- readonly

  Causes a field to become a `displayfield` instead. Boolean fields
  are rendered as "Yes / No" (locale-aware).

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/Utils.js | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 150 insertions(+)

diff --git a/src/Utils.js b/src/Utils.js
index 14a90bf..f41080f 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -1600,6 +1600,156 @@ Ext.define('Proxmox.Utils', {
             }
             return degrees;
         },
+
+        /**
+         * Takes a JSONSchema of a single property and transforms it into an
+         * object representing an ExtJS field.
+         *
+         * @param {string} fieldName - The value for the `name` property of the resulting object.
+         * @param {Object} propSchema - The JSON schema of a property.
+         * @param {Object} extraAttributes - Additional attributes describing the
+         * field which do not exist as keywords in JSON schema.
+         * Note that none of these attributes represent any specific ExtJS property.
+         * @param {Boolean} extraAttributes.readonly - Whether the field shall be read-only.
+         * "Read-only" here means that the field's `xtype` will be set to
+         * `'displayfield'`, regardless of what data type the field will
+         * display. Additionally, checkboxes (booleans) will be displayed as
+         * "Yes" or "No" depending on their value (locale-aware).
+         * @param {Boolean} extraAttributes.sensitive - Whether the field
+         * contains sensitive data, such as a password or similar.
+         * @param {Object} context - The context within which the field is being used.
+         * @param {Boolean} context.isCreate - Whether the field is used when creating a record.
+         */
+        getFieldDefFromPropertySchema: function (fieldName, propSchema, extraAttributes, context) {
+            const fieldPropsByType = {
+                string: {
+                    xtype: 'proxmoxtextfield',
+                    minLength: propSchema.minLength,
+                    maxLength: propSchema.maxLength,
+                },
+                integer: {
+                    xtype: 'proxmoxintegerfield',
+                    minValue: propSchema.minimum,
+                    maxValue: propSchema.maximum,
+                },
+                number: {
+                    xtype: 'numberfield',
+                    minValue: propSchema.minimum,
+                    maxValue: propSchema.maximum,
+                },
+                boolean: {
+                    xtype: 'proxmoxcheckbox',
+                    uncheckedValue: 0,
+                },
+            };
+
+            const commonFieldProps = {
+                name: fieldName,
+                fieldLabel: Ext.htmlEncode(fieldName),
+                emptyText: propSchema.default || '',
+                allowBlank: propSchema.optional,
+            };
+
+            const fieldPropsByFormat = {
+                multiline: {
+                    xtype: 'proxmoxtextarea',
+                },
+            };
+
+            const anyToBool = (value) => {
+                if (typeof value === 'string' || value instanceof String) {
+                    value = value.trim().toLowerCase();
+                    return ['y', 'yes', 'true'].includes(value);
+                }
+
+                return Boolean(value);
+            };
+
+            let fieldProps = fieldPropsByType[propSchema.type];
+            if (fieldProps === undefined) {
+                console.warn(`Unhandled property type '${propSchema.type}'`);
+                fieldProps = fieldPropsByType.string;
+            }
+
+            let formatFieldProps = fieldPropsByFormat[propSchema.format] ?? {};
+
+            let field = {
+                ...fieldProps,
+                ...commonFieldProps,
+                ...formatFieldProps,
+            };
+
+            if (propSchema.title) {
+                field.fieldLabel = Ext.htmlEncode(propSchema.title);
+            }
+
+            if (propSchema.description) {
+                field.autoEl = {
+                    tag: 'div',
+                    'data-qtip': Ext.htmlEncode(Ext.htmlEncode(propSchema.description)),
+                };
+            }
+
+            if (propSchema.enum) {
+                const comboBoxProps = {
+                    xtype: 'proxmoxKVComboBox',
+                    comboItems: propSchema.enum.map((item) => [item, Ext.htmlEncode(item)]),
+                    deleteEmpty: !context.isCreate,
+                    editable: true,
+                };
+
+                Object.assign(field, comboBoxProps);
+            }
+
+            if (context.isCreate && propSchema.default !== undefined) {
+                if (propSchema.type === 'boolean') {
+                    field.checked = anyToBool(propSchema.default);
+                }
+
+                if (field.xtype === 'proxmoxKVComboBox') {
+                    let defaultText =
+                        Proxmox.Utils.defaultText + ' (' + Ext.htmlEncode(propSchema.default) + ')';
+
+                    field.comboItems.unshift(['__default__', defaultText]);
+                    field.value = '__default__';
+                } else {
+                    field.value = propSchema.default;
+                }
+            }
+
+            if (extraAttributes.sensitive) {
+                // textarea fields for multiline strings stay as-is, because
+                // Ext JS will turn them into <input> elements otherwise, which
+                // in turn causes newlines to be replaced with spaces.
+                // 'inputType' unfortunately has no effect for textarea fields.
+                if (field.xtype !== 'proxmoxtextarea') {
+                    field.xtype = 'proxmoxtextfield';
+                    field.inputType = 'password';
+                }
+
+                if (context.isCreate) {
+                    field.value = '';
+                    field.emptyText = Proxmox.Utils.NoneText;
+                } else {
+                    field.emptyText = gettext('Unchanged');
+                }
+            }
+
+            if (extraAttributes.readonly) {
+                field.xtype = 'displayfield';
+
+                if (propSchema.type === 'boolean') {
+                    field.renderer = Proxmox.Utils.format_boolean;
+                }
+
+                if (extraAttributes.sensitive) {
+                    field.value = '*********';
+                    field.emptyText = '';
+                }
+            }
+
+            return field;
+        },
     },
 
     singleton: true,
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH proxmox-widget-toolkit v2 13/17] acme: use helper to construct ExtJS fields from property schemas
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (11 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH proxmox-widget-toolkit v2 12/17] utils: introduce helper function getFieldDefFromPropertySchema Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [PATCH pve-manager v2 14/17] api: add API routes 'plugins' and 'plugins/storage' Max R. Carrara
                   ` (5 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Use the new `getFieldDefFromPropertySchema()` helper function to
simplify some of the logic in `ACMEPluginEdit.js`.

This results in no noticeable difference when setting up ACME challenge
plugins, except for the fact that fields not marked as optional are
now in fact not optional in the UI.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/window/ACMEPluginEdit.js | 42 +++++++++++-------------------------
 1 file changed, 13 insertions(+), 29 deletions(-)

diff --git a/src/window/ACMEPluginEdit.js b/src/window/ACMEPluginEdit.js
index 900923b..d8591ae 100644
--- a/src/window/ACMEPluginEdit.js
+++ b/src/window/ACMEPluginEdit.js
@@ -69,45 +69,29 @@ Ext.define('Proxmox.window.ACMEPluginEdit', {
                 for (const [name, definition] of Object.entries(schema.fields).sort((a, b) =>
                     a[0].localeCompare(b[0]),
                 )) {
-                    let xtype;
-                    switch (definition.type) {
-                        case 'string':
-                            xtype = 'proxmoxtextfield';
-                            break;
-                        case 'integer':
-                            xtype = 'proxmoxintegerfield';
-                            break;
-                        case 'number':
-                            xtype = 'numberfield';
-                            break;
-                        default:
-                            console.warn(`unknown type '${definition.type}'`);
-                            xtype = 'proxmoxtextfield';
-                            break;
-                    }
+                    let fieldName = `custom_${name}`;
+                    let context = { isCreate: me.isCreate };
+
+                    let fieldDef = Proxmox.Utils.getFieldDefFromPropertySchema(
+                        fieldName,
+                        definition,
+                        {},
+                        context,
+                    );
 
                     let label = name;
                     if (typeof definition.name === 'string') {
                         label = definition.name;
                     }
 
-                    let field = Ext.create({
-                        xtype,
-                        name: `custom_${name}`,
+                    let extraProps = {
                         fieldLabel: Ext.htmlEncode(label),
                         width: '100%',
                         labelWidth: 150,
                         labelSeparator: '=',
-                        emptyText: definition.default || '',
-                        autoEl: definition.description
-                            ? {
-                                  tag: 'div',
-                                  'data-qtip': Ext.htmlEncode(
-                                      Ext.htmlEncode(definition.description),
-                                  ),
-                              }
-                            : undefined,
-                    });
+                    };
+
+                    let field = Ext.create({ ...fieldDef, ...extraProps });
 
                     me.createdFields[name] = field;
                     container.add(field);
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH pve-manager v2 14/17] api: add API routes 'plugins' and 'plugins/storage'
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (12 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH proxmox-widget-toolkit v2 13/17] acme: use helper to construct ExtJS fields from property schemas Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-20 12:26   ` Jakob Klocker
  2026-07-17 15:49 ` [PATCH pve-manager v2 15/17] ui: storage view: display error when no editor for storage type exists Max R. Carrara
                   ` (4 subsequent siblings)
  18 siblings, 1 reply; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Add the `plugins` route to our API and handle it via its own module,
`API2/Plugins.pm`. Implement a GET endpoint for this route, which just
lists its subpaths.

Additionally, add the `plugins/storage` path to the API and let all
corresponding endpoints be handled by the
`PVE::API2::Plugins::Storage` module, which is part of the
`libpve-storage-perl` package.

This means that the following routes are now available:
* plugins
* plugins/storage
* plugins/storage/plugin
* plugins/storage/plugin/{type}

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 PVE/API2.pm         |  6 +++++
 PVE/API2/Makefile   |  1 +
 PVE/API2/Plugins.pm | 61 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 68 insertions(+)
 create mode 100644 PVE/API2/Plugins.pm

diff --git a/PVE/API2.pm b/PVE/API2.pm
index 0c7e4654..af1cf38b 100644
--- a/PVE/API2.pm
+++ b/PVE/API2.pm
@@ -17,6 +17,7 @@ use PVE::API2::Nodes;
 use PVE::API2::Pool;
 use PVE::API2::AccessControl;
 use PVE::API2::Storage::Config;
+use PVE::API2::Plugins;
 
 __PACKAGE__->register_method({
     subclass => "PVE::API2::Cluster",
@@ -43,6 +44,11 @@ __PACKAGE__->register_method({
     path => 'pools',
 });
 
+__PACKAGE__->register_method({
+    subclass => "PVE::API2::Plugins",
+    path => 'plugins',
+});
+
 __PACKAGE__->register_method({
     name => 'index',
     path => '',
diff --git a/PVE/API2/Makefile b/PVE/API2/Makefile
index 97f1cc20..97910009 100644
--- a/PVE/API2/Makefile
+++ b/PVE/API2/Makefile
@@ -17,6 +17,7 @@ PERLSOURCE = 			\
 	Network.pm		\
 	NodeConfig.pm		\
 	Nodes.pm		\
+	Plugins.pm		\
 	Pool.pm			\
 	Replication.pm		\
 	ReplicationConfig.pm	\
diff --git a/PVE/API2/Plugins.pm b/PVE/API2/Plugins.pm
new file mode 100644
index 00000000..beb606e6
--- /dev/null
+++ b/PVE/API2/Plugins.pm
@@ -0,0 +1,61 @@
+package PVE::API2::Plugins;
+
+use v5.36;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+use PVE::API2::Plugins::Storage;
+
+__PACKAGE__->register_method({
+    subclass => "PVE::API2::Plugins::Storage",
+    path => 'storage',
+});
+
+__PACKAGE__->register_method({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    permissions => {
+        user => 'all',
+    },
+    description => "Directory index.",
+    parameters => {
+        additionalProperties => 0,
+        properties => {},
+    },
+    returns => {
+        type => 'array',
+        items => {
+            type => 'object',
+            properties => {
+                subdir => {
+                    type => 'string',
+                },
+            },
+        },
+        links => [
+            {
+                rel => 'child',
+                href => "{subdir}",
+            },
+        ],
+    },
+    code => sub($param) {
+        my $result = [];
+
+        my $attrs = PVE::API2::Plugins->method_attributes();
+
+        for my $info ($attrs->@*) {
+            next if !$info->{subclass};
+
+            my $subpath = $info->{match_re}->[0];
+
+            push $result->@*, { subdir => $subpath };
+        }
+
+        return $result;
+    },
+});
+
+1;
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH pve-manager v2 15/17] ui: storage view: display error when no editor for storage type exists
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (13 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH pve-manager v2 14/17] api: add API routes 'plugins' and 'plugins/storage' Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [PATCH pve-manager v2 16/17] ui: storage: add basic UI integration for custom storage plugins Max R. Carrara
                   ` (3 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

... instead of `throw`ing an exception which gets swallowed by ExtJS
and never displayed to the user.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 www/manager6/dc/StorageView.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/www/manager6/dc/StorageView.js b/www/manager6/dc/StorageView.js
index e4c6f07d..bcc02ed5 100644
--- a/www/manager6/dc/StorageView.js
+++ b/www/manager6/dc/StorageView.js
@@ -13,7 +13,8 @@ Ext.define(
         createStorageEditWindow: function (type, sid) {
             let schema = PVE.Utils.storageSchema[type];
             if (!schema || !schema.ipanel) {
-                throw 'no editor registered for storage type: ' + type;
+                Ext.Msg.alert(gettext('Error'), `No editor registered for storage type '${type}'`);
+                return;
             }
 
             Ext.create('PVE.storage.BaseEdit', {
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH pve-manager v2 16/17] ui: storage: add basic UI integration for custom storage plugins
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (14 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH pve-manager v2 15/17] ui: storage view: display error when no editor for storage type exists Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-17 15:49 ` [RFC pve-manager v2 17/17] ui: storage: use property extension keywords for UI hints Max R. Carrara
                   ` (2 subsequent siblings)
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

This commit adds a basic / rudimentary UI integration for custom
storage plugins.

Do this by issuing a request to the new `GET plugins/storage/plugin`
endpoint and checking whether a plugin is custom or not via its Perl
module path. When a user then adds or edits a storage config entry
belonging to a custom storage plugin, the new
`PVE.storage.CustomInputPanel` opens and builds the form's view from
the schemas of the plugin's properties.

In other words, the UI is built completely from the information
provided by the plugin's SectionConfig schema.

It is worth noting that the "Add" dropdown menu button's items are
added to the menu on the fly once the request to
`plugins/storage/plugin` succeeds. For the short moment that the
request is being awaited, the "Add" button is disabled.

This is not noticeable at all for regular (i.e. decently fast)
connections. Users with (very) slow connections will however notice
that the button stays disabled until the request succeeded. If that
were not the case, the dropdown menu of the "Add" button would not
even show up when clicked. Therefore keep the button disabled (greyed
out, unclickable) for the little time that the user cannot do anything
with it anyway.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 www/manager6/Makefile              |   1 +
 www/manager6/dc/StorageView.js     | 131 +++++++++++++-----
 www/manager6/storage/Base.js       |   2 +
 www/manager6/storage/CustomEdit.js | 208 +++++++++++++++++++++++++++++
 4 files changed, 308 insertions(+), 34 deletions(-)
 create mode 100644 www/manager6/storage/CustomEdit.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index d4dd3f35..b10504cd 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -361,6 +361,7 @@ JSSRC= 							\
 	storage/Browser.js				\
 	storage/CIFSEdit.js				\
 	storage/CephFSEdit.js				\
+	storage/CustomEdit.js				\
 	storage/DirEdit.js				\
 	storage/ImageView.js				\
 	storage/IScsiEdit.js				\
diff --git a/www/manager6/dc/StorageView.js b/www/manager6/dc/StorageView.js
index bcc02ed5..7bcb5637 100644
--- a/www/manager6/dc/StorageView.js
+++ b/www/manager6/dc/StorageView.js
@@ -11,20 +11,47 @@ Ext.define(
         stateId: 'grid-dc-storage',
 
         createStorageEditWindow: function (type, sid) {
-            let schema = PVE.Utils.storageSchema[type];
-            if (!schema || !schema.ipanel) {
-                Ext.Msg.alert(gettext('Error'), `No editor registered for storage type '${type}'`);
+            let me = this;
+
+            const metadata = me.pluginMetadata[type];
+
+            // Should never happen, but still handle it here just in case
+            if (!metadata) {
+                Ext.Msg.alert(gettext('Error'), `Plugin '${type}' has no metadata`);
                 return;
             }
 
+            let isCustom = metadata.module.startsWith('PVE::Storage::Custom');
+
+            let paneltype;
+            let canDoBackups;
+
+            if (isCustom) {
+                paneltype = 'PVE.storage.CustomInputPanel';
+                canDoBackups = metadata.content.supported.includes('backup');
+            } else {
+                let schema = PVE.Utils.storageSchema[type];
+                if (!schema || !schema.ipanel) {
+                    Ext.Msg.alert(
+                        gettext('Error'),
+                        `No editor registered for storage type '${type}'`,
+                    );
+                    return;
+                }
+
+                paneltype = 'PVE.storage.' + schema.ipanel;
+                canDoBackups = schema.backups;
+            }
+
             Ext.create('PVE.storage.BaseEdit', {
-                paneltype: 'PVE.storage.' + schema.ipanel,
+                paneltype: paneltype,
                 type: type,
                 storageId: sid,
-                canDoBackups: schema.backups,
+                canDoBackups: canDoBackups,
+                metadata: metadata,
                 autoShow: true,
                 listeners: {
-                    destroy: this.reloadStore,
+                    destroy: me.reloadStore,
                 },
             });
         },
@@ -46,6 +73,69 @@ Ext.define(
 
             let sm = Ext.create('Ext.selection.RowModel', {});
 
+            me.pluginMetadata = {};
+
+            let menuButtonAdd = new Ext.menu.Menu({
+                items: [],
+            });
+
+            let addBtn = new Ext.Button({
+                menu: menuButtonAdd,
+                text: gettext('Add'),
+                disabled: true,
+            });
+
+            let pushBuiltinPluginsToMenu = function () {
+                for (const [type, storage] of Object.entries(PVE.Utils.storageSchema)) {
+                    if (storage.hideAdd) {
+                        continue;
+                    }
+
+                    menuButtonAdd.add({
+                        text: PVE.Utils.format_storage_type(type),
+                        iconCls: 'fa fa-fw fa-' + storage.faIcon,
+                        handler: () => me.createStorageEditWindow(type),
+                    });
+                }
+            };
+
+            let pushCustomPluginsToMenu = function () {
+                for (const type in me.pluginMetadata) {
+                    if (!Object.hasOwn(me.pluginMetadata, type)) {
+                        continue;
+                    }
+
+                    const metadata = me.pluginMetadata[type];
+                    let isCustom = metadata.module.startsWith('PVE::Storage::Custom');
+
+                    if (isCustom) {
+                        menuButtonAdd.add({
+                            text: PVE.Utils.format_storage_type(type),
+                            iconCls: 'fa fa-fw fa-folder',
+                            handler: () => me.createStorageEditWindow(type),
+                        });
+                    }
+                }
+            };
+
+            Proxmox.Utils.API2Request({
+                url: `/api2/extjs/plugins/storage/plugin`,
+                method: 'GET',
+                success: function ({ result: { data } }) {
+                    data.forEach((metadata) => {
+                        me.pluginMetadata[metadata.type] = metadata;
+                    });
+
+                    pushBuiltinPluginsToMenu();
+                    pushCustomPluginsToMenu();
+
+                    addBtn.setDisabled(false);
+                },
+                failure: function ({ htmlStatus }) {
+                    Ext.Msg.alert('Error', htmlStatus);
+                },
+            });
+
             let run_editor = function () {
                 let rec = sm.getSelection()[0];
                 if (!rec) {
@@ -67,24 +157,6 @@ Ext.define(
                 callback: () => store.load(),
             });
 
-            // else we cannot dynamically generate the add menu handlers
-            let addHandleGenerator = function (type) {
-                return function () {
-                    me.createStorageEditWindow(type);
-                };
-            };
-            let addMenuItems = [];
-            for (const [type, storage] of Object.entries(PVE.Utils.storageSchema)) {
-                if (storage.hideAdd) {
-                    continue;
-                }
-                addMenuItems.push({
-                    text: PVE.Utils.format_storage_type(type),
-                    iconCls: 'fa fa-fw fa-' + storage.faIcon,
-                    handler: addHandleGenerator(type),
-                });
-            }
-
             Ext.apply(me, {
                 store: store,
                 reloadStore: () => store.load(),
@@ -92,16 +164,7 @@ Ext.define(
                 viewConfig: {
                     trackOver: false,
                 },
-                tbar: [
-                    {
-                        text: gettext('Add'),
-                        menu: new Ext.menu.Menu({
-                            items: addMenuItems,
-                        }),
-                    },
-                    remove_btn,
-                    edit_btn,
-                ],
+                tbar: [addBtn, remove_btn, edit_btn],
                 columns: [
                     {
                         header: 'ID',
diff --git a/www/manager6/storage/Base.js b/www/manager6/storage/Base.js
index 8961460b..7d416c97 100644
--- a/www/manager6/storage/Base.js
+++ b/www/manager6/storage/Base.js
@@ -99,6 +99,7 @@ Ext.define('PVE.panel.StorageBase', {
                     value: gettext('Keep Snapshots as Volume-Chain enabled if qcow2 images exist!'),
                 });
             }
+            // Note: This hint is also added in CustomEdit.js.
             me.advancedColumnB.unshift({
                 xtype: 'displayfield',
                 name: 'external-snapshot-hint',
@@ -139,6 +140,7 @@ Ext.define('PVE.storage.BaseEdit', {
             type: me.type,
             isCreate: me.isCreate,
             storageId: me.storageId,
+            metadata: me.metadata,
         });
 
         Ext.apply(me, {
diff --git a/www/manager6/storage/CustomEdit.js b/www/manager6/storage/CustomEdit.js
new file mode 100644
index 00000000..45fda7c8
--- /dev/null
+++ b/www/manager6/storage/CustomEdit.js
@@ -0,0 +1,208 @@
+Ext.define('PVE.storage.CustomInputPanel', {
+    extend: 'PVE.panel.StorageBase',
+
+    buildFormFieldFromProperty: function (propertyName) {
+        let me = this;
+        let schema = me.metadata.schema;
+
+        let property = schema[propertyName];
+
+        if (!property) {
+            console.warn(
+                `Tried to create field for unknown property '${propertyName}'` +
+                    ` for storage type '${me.type}'`,
+            );
+            return;
+        }
+
+        const predefinedFields = {
+            content: {
+                xtype: 'pveContentTypeSelector',
+                cts: me.metadata.content.supported,
+                fieldLabel: gettext('Content'),
+                name: 'content',
+                value: me.metadata.content.default,
+                multiSelect: true,
+                allowBlank: false,
+            },
+            preallocation: {
+                xtype: 'pvePreallocationSelector',
+                name: 'preallocation',
+                fieldLabel: gettext('Preallocation'),
+                allowBlank: false,
+                deleteEmpty: !me.isCreate,
+                value: '__default__',
+            },
+            'snapshot-as-volume-chain': {
+                xtype: 'proxmoxcheckbox',
+                name: 'snapshot-as-volume-chain',
+                // TRANSLATORS: As in "a chain of volumes, each referencing the next one".
+                boxLabel: gettext('Allow Snapshots as Volume-Chain'),
+                deleteEmpty: !me.isCreate,
+                // can only allow to enable this on creation for storages that
+                // previously already supported qcow2 to avoid ambiguity with
+                // existing volumes.
+                disabled: !me.isCreate,
+                checked: false,
+            },
+        };
+
+        if (predefinedFields[propertyName]) {
+            return predefinedFields[propertyName];
+        }
+
+        let fieldName = propertyName;
+        let extraAttributes = {
+            readonly: property.fixed && !me.isCreate,
+            sensitive: property.sensitive,
+        };
+        let context = { isCreate: me.isCreate };
+
+        let fieldDef = Proxmox.Utils.getFieldDefFromPropertySchema(
+            fieldName,
+            property,
+            extraAttributes,
+            context,
+        );
+
+        // Fix up any field labels for properties that do not have a title key
+        // by transforming the name of the field like e.g.
+        // "foo_bar-baz" --> "Foo Bar Baz"
+        if (!property.title) {
+            let fieldLabel = fieldName
+                .replace(/([-_]|\s)+/g, ' ')
+                .replace(
+                    /\w*/g,
+                    (label) => label.charAt(0).toUpperCase() + label.slice(1).toLowerCase(),
+                );
+            fieldDef.fieldLabel = Ext.htmlEncode(fieldLabel);
+        }
+
+        return fieldDef;
+    },
+
+    addWidget: function (widget) {
+        let me = this;
+
+        me.column1 = me.column1 || [];
+        me.column2 = me.column2 || [];
+        me.columnB = me.columnB || [];
+
+        const wideFields = [
+            'textarea',
+            'textareafield',
+            'Ext.form.field.TextArea',
+            'proxmoxtextarea',
+            'Proxmox.form.field.TextArea',
+        ];
+
+        if (wideFields.includes(widget.xtype)) {
+            me.columnB.push(widget);
+            return;
+        }
+
+        if (me.column2.length >= me.column1.length) {
+            me.column1.push(widget);
+        } else {
+            me.column2.push(widget);
+        }
+    },
+
+    addAdvancedWidget: function (widget) {
+        let me = this;
+
+        me.advancedColumn1 = me.advancedColumn1 || [];
+        me.advancedColumn2 = me.advancedColumn2 || [];
+
+        if (me.advancedColumn2.length >= me.advancedColumn1.length) {
+            me.advancedColumn1.push(widget);
+        } else {
+            me.advancedColumn2.push(widget);
+        }
+    },
+
+    initComponent: function () {
+        let me = this;
+        let schema = me.metadata.schema;
+
+        me.column1 = me.column1 || [];
+        me.column2 = me.column2 || [];
+
+        const reservedFields = new Set([
+            // automatically added in PVE.panel.StorageBase
+            // --> must not be added here
+            'storage',
+            'nodes',
+            'disable',
+
+            // handled by the "Backup Retention" panel
+            // --> must not be added here
+            'prune-backups',
+            'max-protected-backups',
+
+            // not an actual property, but used by the UI as an inverse of the
+            // 'disable' property
+            // --> must not be added here
+            'enable',
+
+            // not exposed in the UI for any inbuilt storage types
+            // --> must not be added here (in order to remain consistent)
+            'bwlimit',
+            'content-dirs',
+            'create-base-path',
+            'create-subdirs',
+            'format',
+
+            // handled separately for consistency
+            'content',
+            'shared',
+        ]);
+
+        const advancedFields = new Set([
+            'preallocation',
+
+            // note that the technology preview hint is automatically added
+            // further below if a plugin uses this property
+            'snapshot-as-volume-chain',
+        ]);
+
+        // Added first for consistency's sake
+        for (const propertyName of ['content', 'shared']) {
+            let property = schema[propertyName];
+            if (property) {
+                let fieldDef = me.buildFormFieldFromProperty(propertyName);
+                me.column1.push(fieldDef);
+            }
+        }
+
+        for (const propertyName of Object.keys(schema).sort()) {
+            if (reservedFields.has(propertyName)) {
+                continue;
+            }
+
+            let fieldDef = me.buildFormFieldFromProperty(propertyName);
+
+            if (fieldDef === undefined) {
+                continue;
+            }
+
+            if (advancedFields.has(propertyName)) {
+                me.addAdvancedWidget(fieldDef);
+            } else {
+                me.addWidget(fieldDef);
+            }
+
+            if (propertyName === 'snapshot-as-volume-chain') {
+                me.advancedColumnB = me.advancedColumnB || [];
+                me.advancedColumnB.unshift({
+                    xtype: 'displayfield',
+                    name: 'external-snapshot-hint',
+                    userCls: 'pmx-hint',
+                    value: gettext('Snapshots as Volume-Chain are a technology preview.'),
+                });
+            }
+        }
+
+        me.callParent();
+    },
+});
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [RFC pve-manager v2 17/17] ui: storage: use property extension keywords for UI hints
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (15 preceding siblings ...)
  2026-07-17 15:49 ` [PATCH pve-manager v2 16/17] ui: storage: add basic UI integration for custom storage plugins Max R. Carrara
@ 2026-07-17 15:49 ` Max R. Carrara
  2026-07-18 18:19 ` [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Ciro Iriarte
  2026-07-20 12:48 ` Jakob Klocker
  18 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-17 15:49 UTC (permalink / raw)
  To: pve-devel

Use the newly added extension 'x-advanced' and 'x-hidden' extension
keywords for storage plugin property schemas to avoid hard-coding
whether a given field belongs in the "Advanced" section or is hidden,
respectively.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
NOTE: This patch is experimental and thus marked as RFC.

 www/manager6/storage/CustomEdit.js | 24 ++++++------------------
 1 file changed, 6 insertions(+), 18 deletions(-)

diff --git a/www/manager6/storage/CustomEdit.js b/www/manager6/storage/CustomEdit.js
index 45fda7c8..cca66ffb 100644
--- a/www/manager6/storage/CustomEdit.js
+++ b/www/manager6/storage/CustomEdit.js
@@ -145,27 +145,11 @@ Ext.define('PVE.storage.CustomInputPanel', {
             // --> must not be added here
             'enable',

-            // not exposed in the UI for any inbuilt storage types
-            // --> must not be added here (in order to remain consistent)
-            'bwlimit',
-            'content-dirs',
-            'create-base-path',
-            'create-subdirs',
-            'format',
-
             // handled separately for consistency
             'content',
             'shared',
         ]);

-        const advancedFields = new Set([
-            'preallocation',
-
-            // note that the technology preview hint is automatically added
-            // further below if a plugin uses this property
-            'snapshot-as-volume-chain',
-        ]);
-
         // Added first for consistency's sake
         for (const propertyName of ['content', 'shared']) {
             let property = schema[propertyName];
@@ -175,18 +159,22 @@ Ext.define('PVE.storage.CustomInputPanel', {
             }
         }

-        for (const propertyName of Object.keys(schema).sort()) {
+        for (const [propertyName, propertySchema] of Object.entries(schema).sort()) {
             if (reservedFields.has(propertyName)) {
                 continue;
             }

+            if (propertySchema['x-hidden']) {
+                continue;
+            }
+
             let fieldDef = me.buildFormFieldFromProperty(propertyName);

             if (fieldDef === undefined) {
                 continue;
             }

-            if (advancedFields.has(propertyName)) {
+            if (propertySchema['x-advanced']) {
                 me.addAdvancedWidget(fieldDef);
             } else {
                 me.addWidget(fieldDef);
--
2.47.3





^ permalink raw reply related	[flat|nested] 26+ messages in thread

* Re: [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (16 preceding siblings ...)
  2026-07-17 15:49 ` [RFC pve-manager v2 17/17] ui: storage: use property extension keywords for UI hints Max R. Carrara
@ 2026-07-18 18:19 ` Ciro Iriarte
  2026-07-21  9:24   ` Max R. Carrara
  2026-07-20 12:48 ` Jakob Klocker
  18 siblings, 1 reply; 26+ messages in thread
From: Ciro Iriarte @ 2026-07-18 18:19 UTC (permalink / raw)
  To: Max R. Carrara; +Cc: pve-devel

Hi Max,

Since I'm the one who asked for the advanced hint, I should say plainly
that x-advanced and x-hidden are worth keeping, in case the RFC marking
makes them look optional.

>From a third-party plugin author's side this is the difference between an
operator seeing a dozen fields with the four that matter buried among
them, and seeing four fields with a collapsible Advanced section. My
plugin carries management endpoint, credentials, pool, ldev_range,
host_group_prefix, control_plane, QoS limits and consistency-group
options; most deployments touch the endpoint, credentials and pool and
never look at the rest. Without a hint, every one of those is equally
prominent, and the hardcoded advanced list can't help since it only knows
about inbuilt properties.

On patternProperties as the mechanism I have no strong opinion -- that's
a JSONSchema.pm call and you're better placed to make it. A plain
allowlist of known x- keywords would serve me identically. My interest is
only in the capability surviving in some form.

On 'sensitive': I'd expect it in the default schema rather than as an x-
keyword. It describes a property of the data rather than a UI hint, and
the ACME case you mentioned suggests it'll have more than one consumer.

On testing: I'll validate against pve-FCLUPlugin once this lands in the
repo, rather than per-revision while it's still in review -- so expect
feedback from me after it's merged, not a Tested-by on v2.

Thanks,
Ciro



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC pve-common v2 02/17] jsonschema: support 'patternProperties' and allow 'x-' keywords
  2026-07-17 15:49 ` [RFC pve-common v2 02/17] jsonschema: support 'patternProperties' and allow 'x-' keywords Max R. Carrara
@ 2026-07-20 12:01   ` Jakob Klocker
  0 siblings, 0 replies; 26+ messages in thread
From: Jakob Klocker @ 2026-07-20 12:01 UTC (permalink / raw)
  To: pve-devel

comments inline

On 7/17/26 5:49 PM, Max R. Carrara wrote:
> by adding the necessary validation code before the
> `additionalProperties` handling in `check_object()` and extending the
> default schema (the schema for JSON schemas).
> 
> At the same time, allow all keywords beginning with 'x-' to be
> included in a given schema. This makes it possible specify custom
> extension keywords that can be used for e.g. hints, flags, and other
> things.
> 
> Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
> ---
> NOTE: This patch is experimental and thus marked as RFC.
> 
>  src/PVE/JSONSchema.pm    | 61 +++++++++++++++++++++++++++++++++++-----
>  test/json-schema-test.pl |  2 +-
>  2 files changed, 55 insertions(+), 8 deletions(-)
> 
> diff --git a/src/PVE/JSONSchema.pm b/src/PVE/JSONSchema.pm
> index 49f6aa6..1bfa5f6 100644
> --- a/src/PVE/JSONSchema.pm
> +++ b/src/PVE/JSONSchema.pm
> @@ -1073,6 +1073,9 @@ sub parse_property_string {
>      # In property strings we default to not allowing additional properties
>      $additional_properties = 0 if !defined($additional_properties);
> 
> +    # In property strings we do not allow pattern properties at all
> +    my $pattern_properties = 0;
> +
>      # Support named formats here, too:
>      my $validator;
>      if (!ref($format)) {
> @@ -1141,7 +1144,7 @@ sub parse_property_string {
>      }
> 
>      my $errors = {};
> -    check_object($path, $format, $res, $additional_properties, $errors);
> +    check_object($path, $format, $res, $pattern_properties, $additional_properties, $errors);
>      if (scalar(%$errors)) {
>          raise "format error\n", errors => $errors;
>      }
> @@ -1294,7 +1297,7 @@ my sub get_instance_type {
>  }
> 
>  sub check_object {
> -    my ($path, $schema, $value, $additional_properties, $errors) = @_;
> +    my ($path, $schema, $value, $pattern_properties, $additional_properties, $errors) = @_;
> 
>      # print "Check Object " . Dumper($value) . "\nSchema: " . Dumper($schema);
> 
> @@ -1367,6 +1370,23 @@ sub check_object {
>              next if $matched_type; # value is already checked above
>          }
> 
> +        if (defined($pattern_properties) && $pattern_properties) {

checking if the variable is defined and then the variable itself seems
redundant - if `$pattern_properties` is undef the `if` check would be
false anyway
> +            my $has_err = 0;
> +            my $pattern_schema = undef;
> +
> +            for my $pattern (keys $pattern_properties->%*) {
> +                if ($k =~ $pattern) {
> +                    $pattern_schema = $pattern_properties->{$pattern};
> +                    last;
> +                }
> +            }
> +
> +            next if $has_err;

`$has_err` is initialized to 0 and never changed, so this next is dead
code. Was it meant to handle the case where no pattern matches $k?
> +
> +            check_prop($value->{$k}, $pattern_schema, $newpath, $errors);
> +            next;
> +        }
> +
>          if (defined($additional_properties) && !$additional_properties) {
>              add_error(
>                  $errors,
[snip]




^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH pve-storage v2 03/17] api: plugins/storage: add initial routes and endpoints
  2026-07-17 15:49 ` [PATCH pve-storage v2 03/17] api: plugins/storage: add initial routes and endpoints Max R. Carrara
@ 2026-07-20 12:17   ` Jakob Klocker
  2026-07-20 12:21   ` Jakob Klocker
  1 sibling, 0 replies; 26+ messages in thread
From: Jakob Klocker @ 2026-07-20 12:17 UTC (permalink / raw)
  To: Max R. Carrara, pve-devel

comments inline

On 7/17/26 5:50 PM, Max R. Carrara wrote:
> Add the following routes / endpoints:
> 
> * `GET plugins/storage`
>   Subdir index--lists the direct child path components (so, just
>   'plugin' currently).
> 
> * `GET plugins/storage/plugin`
>   Lists all installed storage plugins and their metadata.
>   Currently, this is just an array of objects, with each object
>   containing the plugin's `type()` and Perl module path.
> 
> * `GET plugins/storage/plugin/{type}`
>   Returns the metadata for a single plugin given by `{type}`.
> 
> Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
> ---
>  src/PVE/API2/Makefile                  |   1 +
>  src/PVE/API2/Plugins/Makefile          |  18 +++++
>  src/PVE/API2/Plugins/Storage.pm        |  54 +++++++++++++
>  src/PVE/API2/Plugins/Storage/Makefile  |  17 ++++
>  src/PVE/API2/Plugins/Storage/Plugin.pm | 104 +++++++++++++++++++++++++
>  5 files changed, 194 insertions(+)
>  create mode 100644 src/PVE/API2/Plugins/Makefile
>  create mode 100644 src/PVE/API2/Plugins/Storage.pm
>  create mode 100644 src/PVE/API2/Plugins/Storage/Makefile
>  create mode 100644 src/PVE/API2/Plugins/Storage/Plugin.pm
> 
> diff --git a/src/PVE/API2/Makefile b/src/PVE/API2/Makefile
> index fe316c52..01b7b28f 100644
> --- a/src/PVE/API2/Makefile
> +++ b/src/PVE/API2/Makefile
> @@ -5,3 +5,4 @@ install:
>  	install -D -m 0644 Disks.pm ${DESTDIR}${PERLDIR}/PVE/API2/Disks.pm
>  	make -C Storage install
>  	make -C Disks install
> +	make -C Plugins install
> diff --git a/src/PVE/API2/Plugins/Makefile b/src/PVE/API2/Plugins/Makefile
> new file mode 100644
> index 00000000..f59e75b6
> --- /dev/null
> +++ b/src/PVE/API2/Plugins/Makefile
> @@ -0,0 +1,18 @@
> +SOURCES = Storage.pm		\
> +
> +
> +SUBDIRS = Storage		\
> +
> +
> +INSTALL_PATH = ${DESTDIR}${PERLDIR}/PVE/API2/Plugins
> +
> +
> +.PHONY: install
> +install:
> +	set -e && for SOURCE in ${SOURCES}; \
> +		do install -D -m 0644 $$SOURCE ${INSTALL_PATH}/$$SOURCE; \
> +	done
> +	set -e && for SUBDIR in ${SUBDIRS}; \
> +		do make -C $$SUBDIR install; \
> +	done
> +
> diff --git a/src/PVE/API2/Plugins/Storage.pm b/src/PVE/API2/Plugins/Storage.pm
> new file mode 100644
> index 00000000..53b883fd
> --- /dev/null
> +++ b/src/PVE/API2/Plugins/Storage.pm
> @@ -0,0 +1,54 @@
> +package PVE::API2::Plugins::Storage;
> +
> +use v5.36;
> +
> +use PVE::Storage;
> +use PVE::Storage::Plugin;
> +
> +use PVE::API2::Plugins::Storage::Plugin;
> +
> +use PVE::RESTHandler;
> +use base qw(PVE::RESTHandler);

style-nit: the modules should be split into 4 groups and ordered
alphabetically per the perl style guide [0]. `PVE::RESTHandler;` is a
common module, so it belongs in the same group as `PVE::Storage*`.

I think it should look as following:

use PVE::RESTHandler;
use PVE::Storage;
use PVE::Storage::Plugin;

use PVE::API2::Plugins::Storage::Plugin;

use base qw(PVE::RESTHandler);

[0] https://pve.proxmox.com/wiki/Perl_Style_Guide#Module_Dependencies

> +
> +__PACKAGE__->register_method({
> +    subclass => 'PVE::API2::Plugins::Storage::Plugin',
> +    path => 'plugin',
> +});
> +
> +# plugins/storage
> +
> +__PACKAGE__->register_method({
> +    name => 'index',
> +    path => '',
> +    method => 'GET',
> +    description => 'Directory index.',
> +    permissions => {
> +        user => 'all',
> +    },
> +    parameters => {
> +        additionalProperties => 0,
> +        properties => {},
> +    },
> +    returns => {
> +        type => 'array',
> +        items => {
> +            type => "object",
> +            additionalProperties => 0,
> +            properties => {
> +                subdir => {
> +                    type => 'string',
> +                },
> +            },
> +        },
> +        links => [{ rel => 'child', href => "{subdir}" }],
> +    },
> +    code => sub($param) {
> +        my $result = [
> +            { subdir => 'plugin' },
> +        ];
> +
> +        return $result;
> +    },
> +});
> +
> +1;
> diff --git a/src/PVE/API2/Plugins/Storage/Makefile b/src/PVE/API2/Plugins/Storage/Makefile
> new file mode 100644
> index 00000000..06473a2f
> --- /dev/null
> +++ b/src/PVE/API2/Plugins/Storage/Makefile
> @@ -0,0 +1,17 @@
> +SOURCES = Plugin.pm		\
> +
> +
> +SUBDIRS =
> +
> +
> +INSTALL_PATH = ${DESTDIR}${PERLDIR}/PVE/API2/Plugins/Storage
> +
> +.PHONY: install
> +install:
> +	set -e && for SOURCE in ${SOURCES}; \
> +		do install -D -m 0644 $$SOURCE ${INSTALL_PATH}/$$SOURCE; \
> +	done
> +	set -e && for SUBDIR in ${SUBDIRS}; \
> +		do make -C $$SUBDIR install; \
> +	done
> +
> diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm
> new file mode 100644
> index 00000000..fd0f734b
> --- /dev/null
> +++ b/src/PVE/API2/Plugins/Storage/Plugin.pm
> @@ -0,0 +1,104 @@
> +package PVE::API2::Plugins::Storage::Plugin;
> +
> +use v5.36;
> +
> +use HTTP::Status qw(:constants);
> +
> +use PVE::Exception qw(raise);
> +use PVE::Storage;
> +use PVE::Storage::Plugin;
> +use PVE::Tools qw(extract_param);
> +
> +use PVE::RESTHandler;
> +use base qw(PVE::RESTHandler);
> +

style-nit: same as above

> +my $ALL_PLUGIN_TYPES = PVE::Storage::Plugin->lookup_types();
> +[snip]




^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH pve-storage v2 03/17] api: plugins/storage: add initial routes and endpoints
  2026-07-17 15:49 ` [PATCH pve-storage v2 03/17] api: plugins/storage: add initial routes and endpoints Max R. Carrara
  2026-07-20 12:17   ` Jakob Klocker
@ 2026-07-20 12:21   ` Jakob Klocker
  1 sibling, 0 replies; 26+ messages in thread
From: Jakob Klocker @ 2026-07-20 12:21 UTC (permalink / raw)
  To: Max R. Carrara, pve-devel

missed a nit before

On 7/17/26 5:50 PM, Max R. Carrara wrote:
[snip]
> +        return $result;
> +    },
> +});

style-nit: newline is missing

> +1;





^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH pve-manager v2 14/17] api: add API routes 'plugins' and 'plugins/storage'
  2026-07-17 15:49 ` [PATCH pve-manager v2 14/17] api: add API routes 'plugins' and 'plugins/storage' Max R. Carrara
@ 2026-07-20 12:26   ` Jakob Klocker
  0 siblings, 0 replies; 26+ messages in thread
From: Jakob Klocker @ 2026-07-20 12:26 UTC (permalink / raw)
  To: Max R. Carrara, pve-devel

comment inline

On 7/17/26 5:51 PM, Max R. Carrara wrote:
> Add the `plugins` route to our API and handle it via its own module,
> `API2/Plugins.pm`. Implement a GET endpoint for this route, which just
> lists its subpaths.
> 
> Additionally, add the `plugins/storage` path to the API and let all
> corresponding endpoints be handled by the
> `PVE::API2::Plugins::Storage` module, which is part of the
> `libpve-storage-perl` package.
> 
> This means that the following routes are now available:
> * plugins
> * plugins/storage
> * plugins/storage/plugin
> * plugins/storage/plugin/{type}
> 
> Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
> ---
>  PVE/API2.pm         |  6 +++++
>  PVE/API2/Makefile   |  1 +
>  PVE/API2/Plugins.pm | 61 +++++++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 68 insertions(+)
>  create mode 100644 PVE/API2/Plugins.pm
> 
> diff --git a/PVE/API2.pm b/PVE/API2.pm
> index 0c7e4654..af1cf38b 100644
> --- a/PVE/API2.pm
> +++ b/PVE/API2.pm
> @@ -17,6 +17,7 @@ use PVE::API2::Nodes;
>  use PVE::API2::Pool;
>  use PVE::API2::AccessControl;
>  use PVE::API2::Storage::Config;
> +use PVE::API2::Plugins;
>  

style-nit: modules aren't sorted alphabetically

>  __PACKAGE__->register_method({
>      subclass => "PVE::API2::Cluster",
> @@ -43,6 +44,11 @@ __PACKAGE__->register_method({
>      path => 'pools',
>  });
>  
> +__PACKAGE__->register_method({
> +    subclass => "PVE::API2::Plugins",
> +    path => 'plugins',
> +});
> +
>  __PACKAGE__->register_method({
>      name => 'index',
>      path => '',
> diff --git a/PVE/API2/Makefile b/PVE/API2/Makefile
> index 97f1cc20..97910009 100644
> --- a/PVE/API2/Makefile
> +++ b/PVE/API2/Makefile
> @@ -17,6 +17,7 @@ PERLSOURCE = 			\
>  	Network.pm		\
>  	NodeConfig.pm		\
>  	Nodes.pm		\
> +	Plugins.pm		\
>  	Pool.pm			\
>  	Replication.pm		\
>  	ReplicationConfig.pm	\
> diff --git a/PVE/API2/Plugins.pm b/PVE/API2/Plugins.pm
> new file mode 100644
> index 00000000..beb606e6
> --- /dev/null
> +++ b/PVE/API2/Plugins.pm
> @@ -0,0 +1,61 @@
> +package PVE::API2::Plugins;
[snip]




^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins
  2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (17 preceding siblings ...)
  2026-07-18 18:19 ` [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Ciro Iriarte
@ 2026-07-20 12:48 ` Jakob Klocker
  2026-07-21  9:09   ` Max R. Carrara
  18 siblings, 1 reply; 26+ messages in thread
From: Jakob Klocker @ 2026-07-20 12:48 UTC (permalink / raw)
  To: Max R. Carrara, pve-devel

On 7/17/26 5:50 PM, Max R. Carrara wrote:
> GUI Support for Custom Storage Plugins - v2
> ===========================================
> 
> Notable Changes Since v1
> ------------------------
> 
> - Fix the 'sensitive' keyword being stringified to `"1"` when enabled,
>   instead of being an integer.
>   (Thanks @Jakob!)
> 
> - Fix a non-breaking hyphen sneaking in, most likely caused by me
>   copying an already existing string from the UI or some other place.
>   (Thanks @Jakob!)
> 
> - Implement *experimental* support for `patternProperties` in order to
>   support the custom schema keywords `x-advanced` and `x-hidden`, which
>   helps to avoid hard-coding which fields are in the "Advanced" section
>   and which ones are hidden in the UI, respectively.
> 
>   Please note that the commits implementing this are explicitly marked
>   as RFC and can be dropped if `patternProperties` is undesired.
> 
>   I figured that this was a decent use case for `patternProperties`, but
>   I'm on the fence whether this is something that we actually want.
>   Feedback is very welcome in that regard.
> 
>   Note that the 'sensitive' keyword that's added to the returned plugin
>   schema is kept as is at the moment -- I'm a bit on the fence here too
>   whether it should be an 'x-' keyword or whether we should actually
>   include it in the default schema for JSON schemas overall, as I can
>   see it being useful in other places in the future too, e.g. the ACME
>   plugin code (for hiding API keys etc.).
> 
> 
> Also, given that the two fixes above are rather trivial and no
> functionality has been changed, I suggest keeping Jakob's trailer(s)
> from v1 in, should this series get applied.
> 
[snip]
Thanks for the v2!

I've tested the x-hidden and x-advanced properties by adding them to
the SSHFS plugin; both work as expected. The sensitive keyword now
displays correctly as an integer, and the reported non-breaking hyphen
is replaced with a normal dash.

I've left comments on the patches. Most are style-nits, but there's
one important thing to look into in 2/17.

I also noticed that the module-sorting nit I mentioned isn't applied
consistently across the codebase, so if it's not relevant here, feel
free to ignore it.

The text changes and the backend part look good to me. I'm not too
familiar with the frontend part of PVE yet, so I can't give much input
on that end.




^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins
  2026-07-20 12:48 ` Jakob Klocker
@ 2026-07-21  9:09   ` Max R. Carrara
  0 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-21  9:09 UTC (permalink / raw)
  To: Jakob Klocker, pve-devel

On Mon Jul 20, 2026 at 2:48 PM CEST, Jakob Klocker wrote:
> On 7/17/26 5:50 PM, Max R. Carrara wrote:
> > GUI Support for Custom Storage Plugins - v2
> > ===========================================
> >
> > Notable Changes Since v1
> > ------------------------
> >
> > - Fix the 'sensitive' keyword being stringified to `"1"` when enabled,
> >   instead of being an integer.
> >   (Thanks @Jakob!)
> >
> > - Fix a non-breaking hyphen sneaking in, most likely caused by me
> >   copying an already existing string from the UI or some other place.
> >   (Thanks @Jakob!)
> >
> > - Implement *experimental* support for `patternProperties` in order to
> >   support the custom schema keywords `x-advanced` and `x-hidden`, which
> >   helps to avoid hard-coding which fields are in the "Advanced" section
> >   and which ones are hidden in the UI, respectively.
> >
> >   Please note that the commits implementing this are explicitly marked
> >   as RFC and can be dropped if `patternProperties` is undesired.
> >
> >   I figured that this was a decent use case for `patternProperties`, but
> >   I'm on the fence whether this is something that we actually want.
> >   Feedback is very welcome in that regard.
> >
> >   Note that the 'sensitive' keyword that's added to the returned plugin
> >   schema is kept as is at the moment -- I'm a bit on the fence here too
> >   whether it should be an 'x-' keyword or whether we should actually
> >   include it in the default schema for JSON schemas overall, as I can
> >   see it being useful in other places in the future too, e.g. the ACME
> >   plugin code (for hiding API keys etc.).
> >
> >
> > Also, given that the two fixes above are rather trivial and no
> > functionality has been changed, I suggest keeping Jakob's trailer(s)
> > from v1 in, should this series get applied.
> >
> [snip]
> Thanks for the v2!
>
> I've tested the x-hidden and x-advanced properties by adding them to
> the SSHFS plugin; both work as expected. The sensitive keyword now
> displays correctly as an integer, and the reported non-breaking hyphen
> is replaced with a normal dash.
>
> I've left comments on the patches. Most are style-nits, but there's
> one important thing to look into in 2/17.
>
> I also noticed that the module-sorting nit I mentioned isn't applied
> consistently across the codebase, so if it's not relevant here, feel
> free to ignore it.
>
> The text changes and the backend part look good to me. I'm not too
> familiar with the frontend part of PVE yet, so I can't give much input
> on that end.

Thanks a bunch for the review! I'll see whether I can re-roll this soon
again. Also good find in patch #2, seems like I left some logic behind
from my initial prototype for patternProperties.




^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins
  2026-07-18 18:19 ` [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Ciro Iriarte
@ 2026-07-21  9:24   ` Max R. Carrara
  0 siblings, 0 replies; 26+ messages in thread
From: Max R. Carrara @ 2026-07-21  9:24 UTC (permalink / raw)
  To: Ciro Iriarte; +Cc: pve-devel

On Sat Jul 18, 2026 at 8:19 PM CEST, Ciro Iriarte wrote:
> Hi Max,
>
> Since I'm the one who asked for the advanced hint, I should say plainly
> that x-advanced and x-hidden are worth keeping, in case the RFC marking
> makes them look optional.
>
> From a third-party plugin author's side this is the difference between an
> operator seeing a dozen fields with the four that matter buried among
> them, and seeing four fields with a collapsible Advanced section. My
> plugin carries management endpoint, credentials, pool, ldev_range,
> host_group_prefix, control_plane, QoS limits and consistency-group
> options; most deployments touch the endpoint, credentials and pool and
> never look at the rest. Without a hint, every one of those is equally
> prominent, and the hardcoded advanced list can't help since it only knows
> about inbuilt properties.
>
> On patternProperties as the mechanism I have no strong opinion -- that's
> a JSONSchema.pm call and you're better placed to make it. A plain
> allowlist of known x- keywords would serve me identically. My interest is
> only in the capability surviving in some form.

Hi Ciro!

Thanks a lot for your input, it's really valuable for us to know what
custom plugin authors would like to see in that regard.

The x-advanced and x-hidden keywords will definitely survive in one way
or another; if we decide not to go forward with the patternProperties
approach, I'll probably implement them in the same manner as sensitive
properties, that is, being able to specify them in `plugindata()`.

>
> On 'sensitive': I'd expect it in the default schema rather than as an x-
> keyword. It describes a property of the data rather than a UI hint, and
> the ACME case you mentioned suggests it'll have more than one consumer.

Yeah that's a good point, though ideally I'd like to add such a keyword
as part of a custom vocabulary [vocab] instead (or something similar).
In any case, it's bound to stay in one form or another.

>
> On testing: I'll validate against pve-FCLUPlugin once this lands in the
> repo, rather than per-revision while it's still in review -- so expect
> feedback from me after it's merged, not a Tested-by on v2.

ACK, sounds good! Thanks a lot! There shouldn't really be too many
surprises anyway, since the core logic is pretty fleshed out at this
point, I'd say.

>
> Thanks,
> Ciro

- Max

[vocab] https://json-schema.org/draft/2020-12/json-schema-core#name-schema-vocabularies




^ permalink raw reply	[flat|nested] 26+ messages in thread

end of thread, other threads:[~2026-07-21  9:24 UTC | newest]

Thread overview: 26+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-17 15:49 [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Max R. Carrara
2026-07-17 15:49 ` [PATCH pve-common v2 01/17] json schema: add multiline string format Max R. Carrara
2026-07-17 15:49 ` [RFC pve-common v2 02/17] jsonschema: support 'patternProperties' and allow 'x-' keywords Max R. Carrara
2026-07-20 12:01   ` Jakob Klocker
2026-07-17 15:49 ` [PATCH pve-storage v2 03/17] api: plugins/storage: add initial routes and endpoints Max R. Carrara
2026-07-20 12:17   ` Jakob Klocker
2026-07-20 12:21   ` Jakob Klocker
2026-07-17 15:49 ` [PATCH pve-storage v2 04/17] api: plugins/storage/plugin: include schema in plugin metadata Max R. Carrara
2026-07-17 15:49 ` [PATCH pve-storage v2 05/17] api: plugins/storage/plugin: mark sensitive properties in schema Max R. Carrara
2026-07-17 15:49 ` [PATCH pve-storage v2 06/17] api: plugins/storage/plugin: factor plugin metadata code into helper Max R. Carrara
2026-07-17 15:49 ` [PATCH pve-storage v2 07/17] api: plugins/storage/plugin: add plugins' 'content' to their metadata Max R. Carrara
2026-07-17 15:49 ` [PATCH pve-storage v2 08/17] all plugins: add 'title' to properties, adapt 'description's Max R. Carrara
2026-07-17 15:49 ` [RFC pve-storage v2 09/17] plugin: mark 'preallocation' and 'content-dirs' properties as advanced Max R. Carrara
2026-07-17 15:49 ` [RFC pve-storage v2 10/17] plugin, dirplugin: mark certain properties as hidden Max R. Carrara
2026-07-17 15:49 ` [PATCH proxmox-widget-toolkit v2 11/17] form: introduce new 'proxmoxtextarea' field Max R. Carrara
2026-07-17 15:49 ` [PATCH proxmox-widget-toolkit v2 12/17] utils: introduce helper function getFieldDefFromPropertySchema Max R. Carrara
2026-07-17 15:49 ` [PATCH proxmox-widget-toolkit v2 13/17] acme: use helper to construct ExtJS fields from property schemas Max R. Carrara
2026-07-17 15:49 ` [PATCH pve-manager v2 14/17] api: add API routes 'plugins' and 'plugins/storage' Max R. Carrara
2026-07-20 12:26   ` Jakob Klocker
2026-07-17 15:49 ` [PATCH pve-manager v2 15/17] ui: storage view: display error when no editor for storage type exists Max R. Carrara
2026-07-17 15:49 ` [PATCH pve-manager v2 16/17] ui: storage: add basic UI integration for custom storage plugins Max R. Carrara
2026-07-17 15:49 ` [RFC pve-manager v2 17/17] ui: storage: use property extension keywords for UI hints Max R. Carrara
2026-07-18 18:19 ` [PATCH common/manager/proxmox-widget-toolkit/storage v2 00/17] GUI Support for Custom Storage Plugins Ciro Iriarte
2026-07-21  9:24   ` Max R. Carrara
2026-07-20 12:48 ` Jakob Klocker
2026-07-21  9:09   ` Max R. Carrara

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal