From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id BDB991FF187 for ; Mon, 8 Sep 2025 20:01:23 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 06E4E188BD; Mon, 8 Sep 2025 20:01:17 +0200 (CEST) From: "Max R. Carrara" To: pve-devel@lists.proxmox.com Date: Mon, 8 Sep 2025 20:00:51 +0200 Message-ID: <20250908180058.530119-8-m.carrara@proxmox.com> X-Mailer: git-send-email 2.47.2 In-Reply-To: <20250908180058.530119-1-m.carrara@proxmox.com> References: <20250908180058.530119-1-m.carrara@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1757354450435 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.088 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [RFC pve-storage master v1 07/12] api: views: add paths regarding storage plugin views X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" This commit adds the following paths: - 'plugins/storage/{plugin}/views': Returns an array of objects describing the views that a plugin currently supports. If no views are supported, the array is empty. - 'plugins/storage/{plugin}/views/form': Returns the form view definition of the given plugin. Like in an earlier commit, the form view is always validated against its JSON schema after it was fetched. This is rather suboptimal at the moment and will be done within tests in the future. Right now this is left in so as to keep the RFC smaller. Signed-off-by: Max R. Carrara --- src/PVE/API2/Plugins/Storage/Config.pm | 7 + src/PVE/API2/Plugins/Storage/Makefile | 1 + src/PVE/API2/Plugins/Storage/Views.pm | 172 +++++++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 src/PVE/API2/Plugins/Storage/Views.pm diff --git a/src/PVE/API2/Plugins/Storage/Config.pm b/src/PVE/API2/Plugins/Storage/Config.pm index 60c0515..3f57784 100644 --- a/src/PVE/API2/Plugins/Storage/Config.pm +++ b/src/PVE/API2/Plugins/Storage/Config.pm @@ -18,9 +18,16 @@ use PVE::Storage::Plugin::Meta qw( ); use PVE::Tools qw(extract_param); +use PVE::API2::Plugins::Storage::Views; + use PVE::RESTHandler; use base qw(PVE::RESTHandler); +__PACKAGE__->register_method({ + subclass => 'PVE::API2::Plugins::Storage::Views', + path => '{plugin}/views', +}); + my $PLUGIN_METADATA_SCHEMA = { type => 'object', properties => { diff --git a/src/PVE/API2/Plugins/Storage/Makefile b/src/PVE/API2/Plugins/Storage/Makefile index 73875cf..83fab2e 100644 --- a/src/PVE/API2/Plugins/Storage/Makefile +++ b/src/PVE/API2/Plugins/Storage/Makefile @@ -1,4 +1,5 @@ SOURCES = Config.pm \ + Views.pm \ SUBDIRS = diff --git a/src/PVE/API2/Plugins/Storage/Views.pm b/src/PVE/API2/Plugins/Storage/Views.pm new file mode 100644 index 0000000..7419fb5 --- /dev/null +++ b/src/PVE/API2/Plugins/Storage/Views.pm @@ -0,0 +1,172 @@ +package PVE::API2::Plugins::Storage::Views; + +use v5.36; + +use List::Util qw(any); + +use HTTP::Status qw(:constants); + +use PVE::Exception qw(raise); +use PVE::Storage; +use PVE::Storage::Plugin; +use PVE::Storage::Plugin::Meta qw( + plugin_view_types + plugin_view_modes + get_plugin_metadata +); +use PVE::Storage::Plugin::Views qw( + get_form_view_schema +); +use PVE::Tools qw(extract_param); + +use PVE::RESTHandler; + +use base qw(PVE::RESTHandler); + +# plugins/storage/{plugin}/views + +__PACKAGE__->register_method({ + name => 'index', + path => '', + method => 'GET', + description => "Return available views for a plugin.", + permissions => { + # TODO: perms + description => "", + user => 'all', + }, + parameters => { + additionalProperties => 0, + }, + # NOTE: Intentionally returning an array of objects here for forward compat + returns => { + type => 'array', + items => { + type => 'object', + properties => { + 'view-type' => { + type => 'string', + enum => plugin_view_types(), + optional => 0, + }, + }, + }, + }, + code => sub($param) { + my $param_type = extract_param($param, 'plugin'); + + my $metadata = get_plugin_metadata($param_type) + or raise("Plugin '$param_type' not found", code => HTTP_NOT_FOUND); + + my $result = []; + + for my $view_type ($metadata->{views}->@*) { + my $view_spec = { + 'view-type' => $view_type, + }; + + push($result->@*, $view_spec); + } + + return $result; + }, +}); + +# plugins/storage/{plugin}/views/form + +__PACKAGE__->register_method({ + name => 'form', + path => 'form', + method => 'GET', + description => "Return a plugin's form view.", + permissions => { + # TODO: perms + description => "", + user => 'all', + }, + parameters => { + additionalProperties => 0, + properties => { + plugin => { + type => 'string', + optional => 0, + }, + mode => { + description => "The mode for which to return the view." + . " Can be either 'create' or 'update', depending on whether" + . " the storage is being created or updated (edited).", + type => 'string', + enum => plugin_view_modes(), + optional => 1, + default => 'create', + }, + }, + }, + returns => get_form_view_schema(), + code => sub($param) { + my $param_type = extract_param($param, 'plugin'); + my $param_mode = extract_param($param, 'mode') // 'create'; + + my $metadata = get_plugin_metadata($param_type) + or raise("Plugin '$param_type' not found", code => HTTP_NOT_FOUND); + + my $views = $metadata->{views} // []; + raise("Plugin '$param_type' defines no views", code => HTTP_BAD_REQUEST) + if !scalar($views->@*); + + my $has_form_view = any { $_ eq 'form' } $views->@*; + + raise("Plugin '$param_type' has no form view", code => HTTP_BAD_REQUEST) + if !$has_form_view; + + my $plugin = PVE::Storage::Plugin->lookup($param_type); + + my $context = { + mode => $param_mode, + }; + + my $view = eval { $plugin->get_form_view($context) }; + if (my $err = $@) { + raise( + "Error while fetching form view for plugin '$param_type': $err", + code => HTTP_INTERNAL_SERVER_ERROR, + ); + } + + if (!defined($view)) { + raise( + "Form view for plugin '$param_type' is undefined", + code => HTTP_INTERNAL_SERVER_ERROR, + ); + } + + # TODO: run in tests instead? + # --> test with different contexts (mode only right now) + eval { PVE::JSONSchema::validate($view, get_form_view_schema()); }; + if (my $err = $@) { + # NOTE: left in only for debugging purposes at the moment + require Data::Dumper; + + local $Data::Dumper::Terse = 1; + local $Data::Dumper::Indent = 1; + local $Data::Dumper::Useqq = 1; + local $Data::Dumper::Deparse = 1; + local $Data::Dumper::Quotekeys = 0; + local $Data::Dumper::Sortkeys = 1; + local $Data::Dumper::Trailingcomma = 1; + + warn "Failed to validate form view of plugin '$param_type':\n$err\n"; + warn '$context = ' . Dumper($context) . "\n"; + warn '$view = ' . Dumper($view) . "\n"; + + raise( + "Failed to validate form view of plugin '$param_type':\n$err\n", + code => HTTP_INTERNAL_SERVER_ERROR, + ); + } + + return $view; + }, +}); + +1; -- 2.47.2 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel