* [PATCH storage v6 1/2] fix #7339: lvmthick: add worker to free space of to be deleted VMs
2026-05-22 13:04 [PATCH manager/storage v6 0/2] fix #7339: lvmthick: add option to free storage for deleted VMs Lukas Sichert
@ 2026-05-22 13:04 ` Lukas Sichert
2026-05-22 13:04 ` [PATCH manager v6 2/2] fix #7339: lvmthick: ui: add UI option to free storage Lukas Sichert
1 sibling, 0 replies; 3+ messages in thread
From: Lukas Sichert @ 2026-05-22 13:04 UTC (permalink / raw)
To: pve-devel; +Cc: Lukas Sichert
Currently when deleting a VM whose disk is stored on a
thinly-provisioned LUN there is no way to also free the storage space
used by the VM. This is because the current implementation only calls
'lvremove'. This command deletes the LVM meta-data for the disk, but it
does not send discards to the SAN. 'lvmremove' can also be used with
'issue_discards', but since LVM meta-data is changed, it needs to be
done under a cluster-wide lock, which can lead to timeouts. There is
already an option to enable 'saferemove', which executes 'blkdiscard
--zeroout' to override the whole storage space allocated to the disk
with zeros. However it does not free the storage space.[1]
To add the functionality that frees the storage space, adjust the worker
in the code that is already there for zeroing out. In the worker parse
the storage config for the 'on-volume-remove' property string and check
if the 'discard' property is set. If the option is enabled execute
'blkdiscard'. This can also be executed in combination with 'blkdiscard
--zeroout' to first zero out the disk and then free the storage space
[1]. To allow the frontend to pass the `on-volume-remove` property
string to the backend, add it to the LVM storage plugin schema with the
appropriate format and description, so it is accepted in API requests.
As key and type validation is already enforced by the JSON schema, only
check that `on-volume-remove` is not empty.
[1] https://man7.org/linux/man-pages/man8/blkdiscard.8.html
Signed-off-by: Lukas Sichert <l.sichert@proxmox.com>
---
src/PVE/Storage/LVMPlugin.pm | 78 ++++++++++++++++++++++++++++++++----
1 file changed, 71 insertions(+), 7 deletions(-)
diff --git a/src/PVE/Storage/LVMPlugin.pm b/src/PVE/Storage/LVMPlugin.pm
index 443d292..472176c 100644
--- a/src/PVE/Storage/LVMPlugin.pm
+++ b/src/PVE/Storage/LVMPlugin.pm
@@ -13,6 +13,7 @@ use PVE::Tools qw(run_command file_read_firstline trim);
use PVE::Storage::Common;
use PVE::Storage::Plugin;
+use PVE::SafeSyslog;
use base qw(PVE::Storage::Plugin);
@@ -349,13 +350,28 @@ my sub free_lvm_volumes_locked {
warn $@ if $@;
}
};
-
+ my $on_remove_opts;
+ if ($scfg->{'on-volume-remove'}) {
+ $on_remove_opts =
+ PVE::JSONSchema::parse_property_string('on-volume-remove', $scfg->{'on-volume-remove'});
+ }
# we need to zero out LVM data for security reasons
- # and to allow thin provisioning
- my $zero_out_worker = sub {
+ # and discard images to free storage space to allow
+ # thin provisioning
+ my $cleanup_worker = sub {
+
for my $name (@$volnames) {
my $lvmpath = "/dev/$vg/del-$name";
- print "zero-out data on image $name ($lvmpath)\n";
+
+ my $discard_action;
+ if ($scfg->{saferemove} && $on_remove_opts->{'discard'}) {
+ $discard_action = 'zero-out data and discard (TRIM)';
+ } elsif ($scfg->{saferemove}) {
+ $discard_action = 'zero-out data on';
+ } elsif ($on_remove_opts->{'discard'}) {
+ $discard_action = 'discard (TRIM)';
+ }
+ print "$discard_action image $name ($lvmpath)\n";
my $cmd_activate = ['/sbin/lvchange', '-aly', $lvmpath];
run_command(
@@ -367,8 +383,18 @@ my sub free_lvm_volumes_locked {
$cmd_activate,
errmsg => "can't refresh LV '$lvmpath' to zero-out its data",
);
+ syslog('info', "starting to $discard_action $name ($lvmpath)")
+ if defined($discard_action);
- $secure_delete_cmd->($lvmpath);
+ if ($scfg->{saferemove}) {
+ print "zero-out data on image $name ($lvmpath)\n";
+ $secure_delete_cmd->($lvmpath);
+ }
+ if ($on_remove_opts->{'discard'}) {
+ print "discard image $name ($lvmpath)\n";
+ eval { run_command(['/sbin/blkdiscard', $lvmpath]); };
+ warn $@ if $@;
+ }
$class->cluster_lock_storage(
$storeid,
@@ -383,13 +409,13 @@ my sub free_lvm_volumes_locked {
}
};
- if ($scfg->{saferemove}) {
+ if ($scfg->{saferemove} || $on_remove_opts->{discard}) {
for my $name (@$volnames) {
# avoid long running task, so we only rename here
my $cmd = ['/sbin/lvrename', $vg, $name, "del-$name"];
run_command($cmd, errmsg => "lvrename '$vg/$name' error");
}
- return $zero_out_worker;
+ return $cleanup_worker;
} else {
for my $name (@$volnames) {
my $cmd = ['/sbin/lvremove', '-f', "$vg/$name"];
@@ -412,6 +438,36 @@ sub plugindata {
};
}
+my $on_volume_remove_format = {
+ discard => {
+ description => "Issue discard (TRIM) requests for LVs before removing them.",
+ type => 'boolean',
+ optional => 1,
+ verbose_description => "If enabled, blkdiscard is issued for the LV before removing it."
+ . " This sends discard (TRIM) requests for the LV's block range, allowing"
+ . " thin-provisioned storage to reclaim previously allocated physical"
+ . " space, provided the storage supports discard.",
+ },
+};
+
+sub verify_on_volume_remove {
+ my ($value, $noerr) = @_;
+
+ return undef if !defined($value);
+
+ if (!keys %$value) {
+ return undef if $noerr;
+ die "at least one on-volume-remove option must be specified if the property is set\n";
+ }
+ return $value;
+}
+
+PVE::JSONSchema::register_format(
+ 'on-volume-remove',
+ $on_volume_remove_format,
+ \&verify_on_volume_remove,
+);
+
sub properties {
return {
vgname => {
@@ -428,6 +484,13 @@ sub properties {
description => "Zero-out data when removing LVs.",
type => 'boolean',
},
+ 'on-volume-remove' => {
+ description => "Optional actions when removing LVs.",
+ type => 'string',
+ format => 'on-volume-remove',
+ verbose_description => "Configure actions performed before removing an LV."
+ . " Use 'discard=1' to issue discard (TRIM) requests before removal.",
+ },
'saferemove-stepsize' => {
description => "Wipe step size in MiB."
. " It will be capped to the maximum supported by the storage.",
@@ -453,6 +516,7 @@ sub options {
shared => { optional => 1 },
disable => { optional => 1 },
saferemove => { optional => 1 },
+ 'on-volume-remove' => { optional => 1 },
'saferemove-stepsize' => { optional => 1 },
saferemove_throughput => { optional => 1 },
content => { optional => 1 },
--
2.47.3
^ permalink raw reply related [flat|nested] 3+ messages in thread* [PATCH manager v6 2/2] fix #7339: lvmthick: ui: add UI option to free storage
2026-05-22 13:04 [PATCH manager/storage v6 0/2] fix #7339: lvmthick: add option to free storage for deleted VMs Lukas Sichert
2026-05-22 13:04 ` [PATCH storage v6 1/2] fix #7339: lvmthick: add worker to free space of to be " Lukas Sichert
@ 2026-05-22 13:04 ` Lukas Sichert
1 sibling, 0 replies; 3+ messages in thread
From: Lukas Sichert @ 2026-05-22 13:04 UTC (permalink / raw)
To: pve-devel; +Cc: Lukas Sichert
In the commit 'fix #7339: lvmthick: add worker to free space of to be
deleted VMs' for the 'pve-storage' repo the backend received the
functionality to discard allocated space of a VM disk on a SAN, when a
VM is deleted. The backend checks whether to use this option by parsing
storage.cfg for the 'on-volume-remove' property string and checking if
the 'discard' property is set. The variable 'on-volume-remove' will
automatically be stored into the config file if it is present in the
'PUT' API request.
Expose this option in the GUI by adding a checkbox mapped to the local
form field `on-remove-discard`. Use `cbind` with `isCreate` so the
checkbox is enabled by default only when creating a new LVM storage. Map
`on-remove-discard` to the `discard` property of the `on-volume-remove`
property string. If at least one `on-volume-remove` option is enabled,
serialize the selected options into `on-volume-remove` and include it in
the returned API parameters.
Also rename the 'imgdel' task description from 'Erase data' to 'Destroy
image', since the task can now also discard storage blocks in addition to
zeroing data.
Signed-off-by: Lukas Sichert <l.sichert@proxmox.com>
---
www/manager6/Utils.js | 2 +-
www/manager6/storage/LVMEdit.js | 48 +++++++++++++++++++++++++++++++++
2 files changed, 49 insertions(+), 1 deletion(-)
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 2ed4e65d..0378ecac 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -2179,7 +2179,7 @@ Ext.define('PVE.Utils', {
hastart: ['HA', gettext('Start')],
hastop: ['HA', gettext('Stop')],
imgcopy: ['', gettext('Copy data')],
- imgdel: ['', gettext('Erase data')],
+ imgdel: ['', gettext('Destroy image')],
lvmcreate: [gettext('LVM Storage'), gettext('Create')],
lvmremove: ['Volume Group', gettext('Remove')],
lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')],
diff --git a/www/manager6/storage/LVMEdit.js b/www/manager6/storage/LVMEdit.js
index 148f0601..bdf33ca2 100644
--- a/www/manager6/storage/LVMEdit.js
+++ b/www/manager6/storage/LVMEdit.js
@@ -148,6 +148,39 @@ Ext.define('PVE.storage.LVMInputPanel', {
onlineHelp: 'storage_lvm',
+ onGetValues: function(values) {
+ let me = this;
+
+ let onRemove = {};
+ if (values['on-remove-discard']) {
+ onRemove.discard = 1;
+ }
+ delete values['on-remove-discard'];
+
+ let onRemoveString = PVE.Parser.printPropertyString(onRemove);
+ if (onRemoveString !== '') {
+ values['on-volume-remove'] = onRemoveString;
+ } else if (!me.isCreate) {
+ if (!values.delete) {
+ values.delete = [];
+ }
+ values.delete.push('on-volume-remove');
+ }
+
+ return me.callParent([values]);
+ },
+
+ setValues: function(values) {
+ if (values['on-volume-remove']) {
+ let onRemove = PVE.Parser.parsePropertyString(values['on-volume-remove']);
+ values['on-remove-discard'] = onRemove.discard;
+ }
+
+ delete values['on-volume-remove'];
+
+ return this.callParent([values]);
+ },
+
column1: [
{
xtype: 'pveBaseStorageSelector',
@@ -241,5 +274,20 @@ Ext.define('PVE.storage.LVMInputPanel', {
uncheckedValue: 0,
fieldLabel: gettext('Wipe Removed Volumes'),
},
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'on-remove-discard',
+ uncheckedValue: 0,
+ cbind: {
+ checked: '{isCreate}',
+ },
+ fieldLabel: gettext('Discard Removed Volumes'),
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext(
+ 'Enable to issue discard (TRIM) requests for logical volumes before removing them.',
+ ),
+ },
+ },
],
});
--
2.47.3
^ permalink raw reply related [flat|nested] 3+ messages in thread