[pve-devel] [RFC v1 pve-storage 1/6] version: introduce PVE::Storage::Version

Max Carrara m.carrara at proxmox.com
Thu Jan 30 15:51:19 CET 2025


The purpose of this module is to separate the handling of the Storage
API's version from the remaining code while also providing additional
mechanisms to handle changes to the Storage API more gracefully.

The first such mechanism is the `pveStorageDeprecateAt` attribute,
which, when added to a subroutine, marks the subroutine as deprecated
at a specific version. Should the lowest supported API version
increase beyond the version passed to the attribute, the module will
fail to compile (during Perl's CHECK phase).

This provides a more robust mechanism instead of having to rely on
"FIXME" comments or similar.

Unfortunately attributes can't conveniently be exported using the
`Exporter` module or similar mechanisms, and they can neither be
referenced via their fully qualified path. The attribute's name is
therefore intentionally made long in order to not conflict with any
other possible attributes in the UNIVERSAL namespace.

Signed-off-by: Max Carrara <m.carrara at proxmox.com>
---
 src/PVE/Storage.pm         |  10 +--
 src/PVE/Storage/Makefile   |   1 +
 src/PVE/Storage/Version.pm | 180 +++++++++++++++++++++++++++++++++++++
 3 files changed, 185 insertions(+), 6 deletions(-)
 create mode 100644 src/PVE/Storage/Version.pm

diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index 3b4f041..df4d62f 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -24,6 +24,8 @@ use PVE::RPCEnvironment;
 use PVE::SSHInfo;
 use PVE::RESTEnvironment qw(log_warn);
 
+use PVE::Storage::Version;
+
 use PVE::Storage::Plugin;
 use PVE::Storage::DirPlugin;
 use PVE::Storage::LVMPlugin;
@@ -41,12 +43,8 @@ use PVE::Storage::PBSPlugin;
 use PVE::Storage::BTRFSPlugin;
 use PVE::Storage::ESXiPlugin;
 
-# Storage API version. Increment it on changes in storage API interface.
-use constant APIVER => 10;
-# Age is the number of versions we're backward compatible with.
-# This is like having 'current=APIVER' and age='APIAGE' in libtool,
-# see https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
-use constant APIAGE => 1;
+use constant APIVER => PVE::Storage::Version::APIVER;
+use constant APIAGE => PVE::Storage::Version::APIAGE;
 
 our $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 'vmdk+size', 'zfs', 'btrfs'];
 
diff --git a/src/PVE/Storage/Makefile b/src/PVE/Storage/Makefile
index ce3fd68..5315f69 100644
--- a/src/PVE/Storage/Makefile
+++ b/src/PVE/Storage/Makefile
@@ -1,5 +1,6 @@
 SOURCES= \
 	Common.pm \
+	Version.pm \
 	Plugin.pm \
 	DirPlugin.pm \
 	LVMPlugin.pm \
diff --git a/src/PVE/Storage/Version.pm b/src/PVE/Storage/Version.pm
new file mode 100644
index 0000000..a9216c9
--- /dev/null
+++ b/src/PVE/Storage/Version.pm
@@ -0,0 +1,180 @@
+=head1 NAME
+
+C<PVE::Storage::Version> - Storage API Version Management
+
+=head1 DESCRIPTION
+
+This module is concerned with tracking and managing the version of the Storage
+API interface C<L<PVE::Storage::Plugin>>. Furthermore, this module also provides
+a mechanism to I<deprecate> subroutines that are part of the Storage API via a
+custom I<attribute> called C<L<< pveStorageDeprecateAt|/UNIVERSAL::pveStorageDeprecateAt() >>>.
+
+For more information on this mechanism, see L</"DEPRECATING SUBROUTINES"> below.
+
+B<NOTE:> Importing this module will define the C<L<< pveStorageDeprecateAt|/UNIVERSAL::pveStorageDeprecateAt() >>>
+attribute inside the C<UNIVERSAL> (global) namespace. Ensure that no other
+symbols have the same name as this attribute in order to avoid conflicts.
+
+=cut
+
+package PVE::Storage::Version;
+
+use strict;
+use warnings;
+
+use v5.36;
+
+use Attribute::Handlers;
+use Carp;
+use Scalar::Util qw(reftype);
+
+# NOTE: This module must not import PVE::Storage or any of its submodules,
+# either directly or transitively.
+#
+# Otherwise there's a risk of introducing nasty cyclical imports and/or causing
+# obscure errors during compile time, which may be very hard to debug.
+
+=head1 CONSTANTS
+
+=head3 APIVER
+
+The version of the Storage API.
+
+This version must be incremented if changes to the C<L<PVE::Storage::Plugin>>
+API interface are made.
+
+=head3 APIAGE
+
+The API age is the number of versions we're backward compatible with.
+
+This is similar to having C<current='APIVER'> and C<age='APIAGE'> in C<libtool>.
+For more information, see L<here|https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html>.
+
+Generally, you'll want to increment this when incrementing C<L<< APIVER|/APIVER >>>.
+Resetting C<APIAGE> to C<0> means that backward compatibility is broken and should
+only be done when appropriate (e.g. during a release of a new major version of PVE).
+
+=cut
+
+use constant APIVER => 10;
+use constant APIAGE => 1;
+
+my $MIN_VERSION = (APIVER - APIAGE);
+
+
+=head1 DEPRECATING SUBROUTINES
+
+Upon importing this module, the C<L<< pveStorageDeprecateAt|/UNIVERSAL::pveStorageDeprecateAt >>>
+attribute becomes available. This attribute ought to be used similar to how
+prototypes are used in Perl and requires a C<version> and a C<message> key to
+be passed:
+
+    sub some_sub : pveStorageDeprecateAt(version => 42, message => "...") {
+	# [...]
+    }
+
+Most likely you'll need more space for the C<message>, in which case the
+attribute may simply span multiple lines:
+
+    sub some_sub : pveStorageDeprecateAt(
+	version => 42,
+	message => "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+	    . " Vestibulum ac placerat arcu. Nulla eget risus sed eros pretium"
+	    . " tempus in sed purus. Mauris iaculis ligula justo, in facilisis nisl"
+	    . " maximus at. Aliquam bibendum sapien ut turpis scelerisque, vel"
+	    . " suscipit lorem varius. Mauris et erat posuere, gravida dui"
+	    . " porttitor, faucibus mauris. Phasellus ultricies ante aliquet,"
+	    . " luctus lectus pretium, lobortis arcu. Aliquam fringilla libero id"
+	    . " diam venenatis, nec placerat felis viverra.",
+    ) {
+	# [...]
+    }
+
+As soon as the passed C<version> is below the I<lowest supported version>
+(C<L<< APIVER|/APIVER >>> minus C<L<< APIAGE|/APIAGE >>>), the attribute raises
+an exception during the C<CHECK> phase of the Perl interpreter and also displays
+the passed C<message>. This happens before any runtime code is executed.
+
+Should the passed C<version> be equal to the lowest supported version, a warning
+containing the passed C<message> is emitted whenever the subroutine is called
+instead. This ensures that implementors of custom plugins are made aware of
+subroutines being deprecated, should there be any in use.
+
+The C<L<< pveStorageDeprecateAt|/UNIVERSAL::pveStorageDeprecateAt >>> attribute
+may only be used inside C<L<PVE::Storage>> or its submodules and throws an error
+if used elsewhere.
+
+This mechanism's sole purpose is to enforce deprecation of subroutines during
+"compile time", forcing the subroutine to be removed once the API version is
+incremented too far or the API age is reset.
+
+=head1 ATTRIBUTES
+
+=head3 UNIVERSAL::pveStorageDeprecateAt
+
+This attribute marks subroutines as deprecated at a specific C<version> and will
+emit an error with the given C<message> if the lowest supported Storage API
+version exceeds the C<version> passed to the attribute.
+
+    sub some_sub : pveStorageDeprecateAt(version => 42, message => "...") {
+	# [...]
+    }
+
+    sub some_sub : pveStorageDeprecateAt(
+	version => 42,
+	message => "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+	    . " Vestibulum ac placerat arcu. Nulla eget risus sed eros pretium"
+	    . " tempus in sed purus. Mauris iaculis ligula justo, in facilisis nisl"
+	    . " maximus at. Aliquam bibendum sapien ut turpis scelerisque, vel"
+	    . " suscipit lorem varius. Mauris et erat posuere, gravida dui"
+	    . " porttitor, faucibus mauris. Phasellus ultricies ante aliquet,"
+	    . " luctus lectus pretium, lobortis arcu. Aliquam fringilla libero id"
+	    . " diam venenatis, nec placerat felis viverra.",
+    ) {
+	# [...]
+    }
+
+Should the passed C<version> be equal to the currently lowest supported API
+version, a warning containing the passed C<message> is emitted instead.
+
+=cut
+
+sub UNIVERSAL::pveStorageDeprecateAt : ATTR(CODE,CHECK) {
+    my ($package, $symbol, $referent, $attr_name, $attr_data, $phase, $filename, $line) = @_;
+
+    confess "'$attr_name' attribute may only be used inside PVE::Storage and its submodules"
+	if $package !~ m/^PVE::Storage/;
+
+    my $data = { $attr_data->@* };
+
+    my ($version, $message) = ($data->{version}, $data->{message});
+    my $reftype = reftype($referent);
+
+    confess "Referent is not a reference"
+	. " -- '$attr_name' attribute may only be used for subroutines"
+	if !defined($reftype);
+
+    confess "Unexpected reftype '$reftype'"
+	. " -- '$attr_name' attribute may only be used for subroutines"
+	if !($reftype eq 'CODE' || $reftype eq 'ANON');
+
+    confess "Either version or message not defined"
+	. " -- usage: $attr_name(version => 42, message => \"...\")"
+	if !defined($version) || !defined($message);
+
+    confess "Storage API version '$version' not supported: $message"
+	if $version < $MIN_VERSION;
+
+    no warnings 'redefine';
+    *$symbol = sub {
+	my $symbol_name = *{$symbol}{"NAME"};
+	carp "Warning: subroutine '$symbol_name' is deprecated - $message"
+	    if $version == $MIN_VERSION;
+	$referent->(@_);
+    };
+
+    return;
+}
+
+
+1;
-- 
2.39.5





More information about the pve-devel mailing list