From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id D68311FF0AF for ; Fri, 06 Nov 2026 16:46:41 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 1080921706; Fri, 06 Nov 2026 16:46:17 +0100 (CET) From: "Max R. Carrara" To: pve-devel@lists.proxmox.com Subject: [PATCH pve-storage v4 03/17] api: plugins/storage/plugin: include schema in plugin metadata Date: Fri, 6 Nov 2026 16:45:39 +0100 Message-ID: <20261106154554.2182986-4-m.carrara@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261106154554.2182986-1-m.carrara@proxmox.com> References: <20261106154554.2182986-1-m.carrara@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793979961115 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.338 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 6IWEI35VU3CWNF67DBD7URLTACRU7KFG X-Message-ID-Hash: 6IWEI35VU3CWNF67DBD7URLTACRU7KFG X-MailFrom: m.carrara@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Alfonso Kuen , Thomas Ellmenreich , Jakob Klocker X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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, that is, 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 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. Whether a property is optional for a plugin is taken from the plugin's `options()` only, which is also what is enforced when the config is checked. The property schemas themselves cannot be used for that, as the global property list marks every plugin property as optional. Signed-off-by: Max R. Carrara --- src/PVE/API2/Plugins/Storage/Plugin.pm | 52 ++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm index e5d6b04f..07909c40 100644 --- a/src/PVE/API2/Plugins/Storage/Plugin.pm +++ b/src/PVE/API2/Plugins/Storage/Plugin.pm @@ -20,18 +20,68 @@ my $PLUGIN_METADATA_SCHEMA = { properties => { module => { type => 'string', optional => 0, }, + schema => { + type => 'object', + optional => 0, + }, type => { type => 'string', optional => 0, enum => $ALL_PLUGIN_TYPES, }, }, }; +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}; + } + + # NOTE: The global property list in SectionConfig marks every plugin + # property as optional. This means that we cannot rely on the + # `optional` key that the `get_property_schema` above returns for + # validation in the front end; it would mark required values as + # optional. + # + # Note that we also cannot rely on `createSchema()` / `updateSchema()` + # here for the same reason. + # + # Therefore, we override this key below with what the plugin's + # `options()` set directly. This makes it so that the front end + # validation matches the back end's. + # + # See the docstring for PVE::SectionConfig::properties() for more + # information. + my $is_optional = $options->{$option}->{optional}; + $property->{optional} = $is_optional ? 1 : 0; + } + + return $schema; +} + # plugins/storage/plugin __PACKAGE__->register_method({ name => 'index', path => '', @@ -54,10 +104,11 @@ __PACKAGE__->register_method({ 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, }; push($result->@*, $item); } @@ -94,10 +145,11 @@ __PACKAGE__->register_method({ 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; }, -- 2.47.3