public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Thomas Lamprecht <t.lamprecht@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH v3 manager 73/76] ui: storage wizard: let root@pam wipe a LUN with data
Date: Fri,  6 Nov 2026 00:31:48 +0100	[thread overview]
Message-ID: <20261105233336.2178064-74-t.lamprecht@proxmox.com> (raw)
In-Reply-To: <20261105233336.2178064-1-t.lamprecht@proxmox.com>

Let root@pam wipe a data-bearing LUN without leaving shared LVM setup,
using the Node > SAN confirmation and refusal checks. Unknown usage or
volume-group membership never makes a device wipeable.

Track the same wipe across scans. Block selection and submission until
its outcome is known: resume checks for known tasks, but require a
task-history check before dismissing an unanswered request.

After failed Fibre Channel volume-group creation, recheck the original
device and storage configuration before offering recovery on Confirm.
Distinguish an untouched LUN, an existing group, and a refused wipe from
probe failures. Discard obsolete recovery attempts and block recovery
while creation is pending. Verify the pinned device is unused after
every wipe outcome before retrying. Display-only devices remain
ineligible for wiping.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
 www/manager6/Makefile                       |   2 +
 www/manager6/SANUtils.js                    |   8 +-
 www/manager6/storage/wizard/FCRecovery.js   | 242 ++++++++++++++++++++
 www/manager6/storage/wizard/FibreChannel.js |  89 ++++++-
 www/manager6/storage/wizard/LunPicker.js    |  70 ++++++
 www/manager6/storage/wizard/LunSelect.js    |  70 ++++--
 www/manager6/storage/wizard/WipeStatus.js   |  34 +++
 www/manager6/storage/wizard/Wizard.js       | 116 +++++++++-
 8 files changed, 597 insertions(+), 34 deletions(-)
 create mode 100644 www/manager6/storage/wizard/FCRecovery.js
 create mode 100644 www/manager6/storage/wizard/WipeStatus.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 8717d041a..8b410d91c 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -390,11 +390,13 @@ JSSRC= 							\
 	storage/wizard/CommonSettings.js		\
 	storage/wizard/LunPicker.js			\
 	storage/wizard/LunSelect.js			\
+	storage/wizard/WipeStatus.js			\
 	storage/wizard/NodeIdentities.js		\
 	storage/wizard/SanLvm.js			\
 	storage/wizard/NFS.js				\
 	storage/wizard/CIFS.js				\
 	storage/wizard/IScsi.js				\
+	storage/wizard/FCRecovery.js			\
 	storage/wizard/FibreChannel.js			\
 	storage/wizard/ZFSOverISCSI.js			\
 	Workspace.js					\
diff --git a/www/manager6/SANUtils.js b/www/manager6/SANUtils.js
index 7bbb287e2..801f77777 100644
--- a/www/manager6/SANUtils.js
+++ b/www/manager6/SANUtils.js
@@ -367,10 +367,14 @@ Ext.define('PVE.SANUtils', {
         return Number(response?.result?.status) === 403;
     },
 
-    // returns { text, tip } as HTML; an attribution to ownStorage alone does not count as use
-    san_lun_usage: function (row, { ownStorage } = {}) {
+    // returns { text, tip } as HTML; an attribution to ownStorage alone does not count as use, and
+    // refusal says why the row cannot be wiped
+    san_lun_usage: function (row, { ownStorage, refusal } = {}) {
         let text = PVE.SANUtils.san_lun_usage_text(row, ownStorage);
         let tooltip = [text];
+        if (refusal) {
+            tooltip.push(Ext.htmlEncode(refusal));
+        }
         if (row['duplicate-pv']) {
             let tip = gettext(
                 'Another LUN has an LVM physical volume with the same UUID. LVM refuses to activate or change its volume group.',
diff --git a/www/manager6/storage/wizard/FCRecovery.js b/www/manager6/storage/wizard/FCRecovery.js
new file mode 100644
index 000000000..d4a337c1c
--- /dev/null
+++ b/www/manager6/storage/wizard/FCRecovery.js
@@ -0,0 +1,242 @@
+Ext.define('PVE.storage.wizard.FCRecovery', {
+    extend: 'Ext.container.Container',
+    xtype: 'pveStorageWizardFCRecovery',
+    layout: { type: 'vbox', align: 'stretch' },
+
+    invalidate: function () {
+        delete this.attempt;
+        this.hide();
+    },
+
+    initComponent: function () {
+        let me = this;
+        me.callParent();
+        // only added to the confirm tab of a constructed wizard
+        me.on(
+            'added',
+            function () {
+                let wizard = me.up('window');
+                me.mon(wizard, 'setupchange', function () {
+                    me.down('#wipe').setDisabled(me.blocked());
+                    me.down('#check').setDisabled(me.blocked());
+                });
+                me.mon(wizard.down('#wizcontent'), 'tabchange', function (tp, tab) {
+                    if (tab !== wizard.down('#confirm')) {
+                        me.invalidate();
+                    }
+                });
+            },
+            me,
+            { single: true },
+        );
+    },
+
+    discover: function (node, lun, vgname) {
+        let me = this;
+        let field = 'wwid';
+        me.attempt = { node, identity: lun?.[field], field, vgname };
+        me.check();
+    },
+
+    blocked: function () {
+        let wizard = this.up('window');
+        return !!(
+            wizard.busy ||
+            wizard.wipeOperation ||
+            wizard.created?.task ||
+            wizard.created?.volumeGroup ||
+            wizard.unconfirmedStorage ||
+            wizard.unknownVolumeGroup
+        );
+    },
+
+    check: function () {
+        let me = this;
+        let wizard = me.up('window');
+        let attempt = me.attempt;
+        if (!attempt || me.blocked()) {
+            return;
+        }
+        let token = (me.checkToken = (me.checkToken ?? 0) + 1);
+        let current = () => !me.destroyed && me.attempt === attempt && token === me.checkToken;
+        me.show();
+        me.down('#wipe').hide();
+        me.down('#check').hide();
+        me.down('#note').setValue(gettext('Checking the selected device...'));
+        let failure = function () {
+            if (!current()) {
+                return;
+            }
+            me.down('#note').setValue(
+                Ext.htmlEncode(
+                    Ext.String.format(
+                        gettext(
+                            'Could not check LUN "{0}" for a leftover physical volume. Press Check Again.',
+                        ),
+                        attempt.identity || Proxmox.Utils.unknownText,
+                    ),
+                ),
+            );
+            me.down('#check').show();
+        };
+        if (!attempt.identity || attempt.identity === 'unknown') {
+            failure();
+            return;
+        }
+        Proxmox.Utils.API2Request({
+            url: `/nodes/${attempt.node}/scan/san-luns`,
+            method: 'GET',
+            failure,
+            success: function (response) {
+                if (!current()) {
+                    return;
+                }
+                let rows = PVE.SANUtils.group_san_lun_paths(response.result.data || []).filter(
+                    (row) => row[attempt.field] === attempt.identity,
+                );
+                if (rows.length !== 1) {
+                    failure();
+                    return;
+                }
+                let lun = rows[0];
+                Proxmox.Utils.API2Request({
+                    url: '/storage',
+                    method: 'GET',
+                    failure,
+                    success: function (config) {
+                        if (!current()) {
+                            return;
+                        }
+                        if (
+                            attempt.verifyUnused &&
+                            lun.usage === 'unused' &&
+                            !lun['used-by-storage'] &&
+                            lun['in-use'] === 'no'
+                        ) {
+                            attempt.ready = true;
+                            me.down('#note').setValue(
+                                Ext.htmlEncode(
+                                    Ext.String.format(
+                                        gettext(
+                                            'LUN "{0}" is now unused. Press Retry Setup to create volume group "{1}" on this LUN.',
+                                        ),
+                                        attempt.identity,
+                                        attempt.vgname,
+                                    ),
+                                ),
+                            );
+                            wizard.updateActions();
+                            return;
+                        }
+                        if (lun.usage === 'unused' && !attempt.verifyUnused) {
+                            me.down('#note').setValue(
+                                Ext.htmlEncode(
+                                    Ext.String.format(
+                                        gettext(
+                                            'No leftover physical volume was found on LUN "{0}".',
+                                        ),
+                                        attempt.identity,
+                                    ),
+                                ),
+                            );
+                            return;
+                        }
+                        if (!['lvm', 'other'].includes(lun.usage)) {
+                            failure();
+                            return;
+                        }
+                        // an empty VG name alone does not prove an orphan PV
+                        let refusal = PVE.window.SANLunWipe.reason(lun, config.result.data || []);
+                        if (refusal) {
+                            me.down('#note').setValue(Ext.htmlEncode(refusal));
+                            me.down('#check').show();
+                            return;
+                        }
+                        if (lun.vgname) {
+                            me.down('#note').setValue(
+                                Ext.htmlEncode(
+                                    Ext.String.format(
+                                        gettext(
+                                            'LUN "{0}" contains volume group "{1}". If this setup created it, complete the setup under Datacenter > Storage > Add > LVM.',
+                                        ),
+                                        attempt.identity,
+                                        lun.vgname,
+                                    ),
+                                ),
+                            );
+                            return;
+                        }
+                        if (lun.usage !== 'lvm') {
+                            failure();
+                            return;
+                        }
+                        attempt.lun = lun;
+                        me.down('#note').setValue(
+                            Ext.htmlEncode(
+                                Ext.String.format(
+                                    gettext(
+                                        'An LVM physical volume without a volume group remains on LUN "{0}" on node "{1}". Wipe it only if it is no longer needed anywhere.',
+                                    ),
+                                    attempt.identity,
+                                    attempt.node,
+                                ),
+                            ),
+                        );
+                        me.down('#wipe').show();
+                        wizard.updateActions();
+                    },
+                });
+            },
+        });
+    },
+
+    wipe: function () {
+        let me = this;
+        let wizard = me.up('window');
+        let attempt = me.attempt;
+        if (!attempt?.lun || Proxmox.UserName !== 'root@pam' || me.blocked()) {
+            return;
+        }
+        wizard.openWipe(attempt.node, attempt.lun, function (wiped) {
+            if (me.destroyed || me.attempt !== attempt) {
+                return;
+            }
+            if (wizard.created?.entry) {
+                attempt.verifyUnused = true;
+                attempt.ready = false;
+                me.check();
+            } else {
+                wizard.down('pveStorageWizardFCLunSelect').refreshLuns();
+                if (wiped) {
+                    me.show();
+                    me.down('#note').setValue(
+                        gettext(
+                            'Wipe completed. Go back to the SAN LUN step and select a LUN before retrying.',
+                        ),
+                    );
+                    me.down('#wipe').hide();
+                    me.down('#check').hide();
+                }
+            }
+        });
+    },
+
+    items: [
+        { xtype: 'displayfield', itemId: 'note' },
+        {
+            xtype: 'button',
+            itemId: 'check',
+            text: gettext('Check Again'),
+            hidden: true,
+            handler: (button) => button.up('pveStorageWizardFCRecovery').check(),
+        },
+        {
+            xtype: 'button',
+            itemId: 'wipe',
+            text: gettext('Wipe...'),
+            hidden: true,
+            cls: 'pve-dangerous-button',
+            handler: (button) => button.up('pveStorageWizardFCRecovery').wipe(),
+        },
+    ],
+});
diff --git a/www/manager6/storage/wizard/FibreChannel.js b/www/manager6/storage/wizard/FibreChannel.js
index b0f5922f4..b44ec393a 100644
--- a/www/manager6/storage/wizard/FibreChannel.js
+++ b/www/manager6/storage/wizard/FibreChannel.js
@@ -121,21 +121,39 @@ Ext.define('PVE.storage.wizard.FCLunSelect', {
         return check;
     },
 
+    wipeable: function (rec) {
+        let me = this;
+        return (
+            !!rec &&
+            me.allowWipe &&
+            !rec.get('reason') &&
+            !PVE.window.SANLunWipe.reason(rec.data, me.storageConfig)
+        );
+    },
+
     validator: function () {
         let wizard = this.up('window');
-        return !wizard.busy && !!wizard.getViewModel().get('vgmode');
+        return !wizard.busy && !wizard.wipeOperation && !!wizard.getViewModel().get('vgmode');
     },
 
     updateActions: function () {
         let me = this;
         let wizard = me.up('window');
         let pinned = !!wizard.created?.entry;
-        let blocked = wizard.busy || pinned;
+        let blocked = wizard.busy || !!wizard.wipeOperation || pinned;
         let grid = me.down('#lunGrid');
         grid.setDisabled(blocked || me.scanning);
+        let optin = me.down('#allowWipe');
+        optin.setDisabled(blocked);
+        optin.setHidden(pinned || Proxmox.UserName !== 'root@pam');
+        if (pinned && optin.getValue()) {
+            optin.setValue(false);
+        }
         me.down('#rescan').setHidden(pinned);
+        me.down('#wipeButton').setHidden(pinned || !me.allowWipe);
         me.down('#rescan').setDisabled(blocked);
         me.down('pveStorageScanNodeSelector')?.setDisabled(blocked);
+        me.down('#wipeButton').setDisabled(blocked || !me.wipeable(grid.getSelection()[0]));
         if (wizard.down('#wizcontent').getActiveTab() === me) {
             wizard.down('#next').setDisabled(!me.validator());
         }
@@ -216,6 +234,7 @@ Ext.define('PVE.storage.wizard.FCLunSelect', {
         let grid = me.down('#lunGrid');
         me.scanning = true;
         me.scanDone = false;
+        wizard.down('pveStorageWizardFCRecovery')?.invalidate();
         grid.getSelectionModel().deselectAll();
         me.selection = undefined;
         me.lunScan = undefined;
@@ -406,6 +425,7 @@ Ext.define('PVE.storage.wizard.FCLunSelect', {
         let updateSelection = function (rec) {
             let wizard = me.up('window');
             let multipath = rec?.get('multipath');
+            wizard.down('pveStorageWizardFCRecovery')?.invalidate();
             delete me.entryCheck;
             me.selection = rec
                 ? {
@@ -421,6 +441,8 @@ Ext.define('PVE.storage.wizard.FCLunSelect', {
                   }
                 : undefined;
             let vm = wizard.getViewModel();
+            me.down('#wipeButton')?.setDisabled(!me.wipeable(rec));
+            // a row selected only for wiping is not the storage's LUN, so the step stays incomplete
             if (!rec || !rec.get('selectable')) {
                 vm.set({ vgmode: '', device: '', vgname: '' });
             } else if (rec.get('usage') === 'unused') {
@@ -439,6 +461,7 @@ Ext.define('PVE.storage.wizard.FCLunSelect', {
                 xtype: 'displayfield',
                 value: gettext('Select the SAN LUN for the volume group:'),
             },
+            { xtype: 'pveStorageWizardWipeStatus' },
             {
                 xtype: 'grid',
                 itemId: 'lunGrid',
@@ -446,10 +469,11 @@ Ext.define('PVE.storage.wizard.FCLunSelect', {
                 store: store,
                 viewConfig: {
                     trackOver: false,
-                    getRowClass: (rec) => (rec.get('selectable') ? '' : 'x-item-disabled'),
+                    getRowClass: (rec) =>
+                        rec.get('selectable') || me.wipeable(rec) ? '' : 'x-item-disabled',
                 },
                 listeners: {
-                    beforeselect: (grid, rec) => rec.get('selectable'),
+                    beforeselect: (grid, rec) => rec.get('selectable') || me.wipeable(rec),
                     selectionchange: (sm, selection) => updateSelection(selection[0]),
                 },
                 columns: [
@@ -498,15 +522,21 @@ Ext.define('PVE.storage.wizard.FCLunSelect', {
             {
                 xtype: 'container',
                 layout: 'hbox',
-                items: [
-                    {
-                        xtype: 'button',
-                        itemId: 'rescan',
-                        text: gettext('Rescan'),
-                        iconCls: 'fa fa-search',
-                        handler: () => me.refreshLuns(),
+                items: me.wipeControls(() => me.refreshLuns(), {
+                    margin: '0 0 0 10',
+                    handler: function () {
+                        let rec = me.down('#lunGrid').getSelection()[0];
+                        if (!me.wipeable(rec)) {
+                            return;
+                        }
+                        let wizard = me.up('window');
+                        wizard.openWipe(wizard.getScanNode(), rec.data, () => {
+                            if (!me.destroyed) {
+                                me.refreshLuns();
+                            }
+                        });
                     },
-                ],
+                }),
             },
             {
                 xtype: 'proxmoxcheckbox',
@@ -564,6 +594,7 @@ Ext.define('PVE.storage.wizard.FCLunSelect', {
                 xtype: 'pveStorageScanNodeSelector',
                 listeners: {
                     change: function () {
+                        me.up('window').down('pveStorageWizardFCRecovery')?.invalidate();
                         me.refreshLuns();
                     },
                 },
@@ -639,7 +670,16 @@ PVE.storage.wizard.types.fc = {
         ],
     },
 
+    confirmItems: () => [
+        { xtype: 'pveStorageWizardWipeStatus' },
+        { xtype: 'pveStorageWizardFCRecovery', hidden: true },
+    ],
+
     canSubmit: function (wizard) {
+        let attempt = wizard.down('pveStorageWizardFCRecovery')?.attempt;
+        if (attempt?.verifyUnused && !attempt.ready) {
+            return false;
+        }
         let values = wizard.getValues();
         let step = wizard.down('pveStorageWizardFCLunSelect');
         let check = step.checkMultipathEntry(values.nodes);
@@ -760,6 +800,7 @@ PVE.storage.wizard.types.fc = {
             );
             return;
         }
+        wizard.down('pveStorageWizardFCRecovery')?.invalidate();
         let enc = Ext.htmlEncode;
         let mode = values.vgmode;
         delete values.vgmode;
@@ -892,6 +933,13 @@ PVE.storage.wizard.types.fc = {
             poll();
         };
 
+        let failedLun = Ext.clone(step.selection.lun);
+        let offerWipe = function () {
+            if (Proxmox.UserName === 'root@pam') {
+                wizard.down('pveStorageWizardFCRecovery').discover(node, failedLun, params.vgname);
+            }
+        };
+
         let volumeGroupFailure = function (msg, final) {
             if (final) {
                 delete created.task;
@@ -899,6 +947,22 @@ PVE.storage.wizard.types.fc = {
                 if (!created.entry) {
                     wizard.unfreeze();
                 }
+                offerWipe();
+                if (Proxmox.UserName === 'root@pam') {
+                    msg = Ext.String.format(
+                        gettext(
+                            'Volume group setup did not complete: {0}. The Confirm page checks this LUN for a leftover physical volume and offers Wipe... if the local checks allow it. If volume group "{1}" already exists, complete the setup under Datacenter > Storage > Add > LVM.',
+                        ),
+                        msg,
+                        enc(params.vgname),
+                    );
+                } else {
+                    msg +=
+                        '<br><br>' +
+                        gettext(
+                            'If a leftover physical volume remains, root@pam can wipe it under Node > SAN before retrying. If the volume group already exists, complete the setup under Datacenter > Storage > Add > LVM.',
+                        );
+                }
             } else {
                 msg +=
                     '<br><br>' +
@@ -941,6 +1005,7 @@ PVE.storage.wizard.types.fc = {
                     ),
                 },
                 success: function () {
+                    wizard.down('pveStorageWizardFCRecovery')?.invalidate();
                     created.volumeGroup = params.vgname;
                     delete created.task;
                     next();
diff --git a/www/manager6/storage/wizard/LunPicker.js b/www/manager6/storage/wizard/LunPicker.js
index 7ae6b0564..5bcaace44 100644
--- a/www/manager6/storage/wizard/LunPicker.js
+++ b/www/manager6/storage/wizard/LunPicker.js
@@ -1,3 +1,4 @@
+// the picker provides wipeable(rec) and a #lunGrid
 Ext.define('PVE.storage.wizard.LunPicker', {
     extend: 'Ext.Mixin',
 
@@ -5,6 +6,75 @@ Ext.define('PVE.storage.wizard.LunPicker', {
         id: 'lunPicker',
     },
 
+    allowWipe: false,
+    storageConfig: undefined,
+    allowWipeText: gettext('Allow wiping LUNs with data'),
+
+    setAllowWipe: function (allow) {
+        let me = this;
+        let grid = me.down('#lunGrid');
+        let token = (me.wipeConfigToken = (me.wipeConfigToken ?? 0) + 1);
+        let apply = function (storages) {
+            if (me.destroyed || token !== me.wipeConfigToken) {
+                return;
+            }
+            me.allowWipe = allow;
+            me.storageConfig = storages;
+            me.down('#wipeButton').setHidden(!allow);
+            let rec = grid.getSelection()[0];
+            if (rec && !rec.get('selectable') && !me.wipeable(rec)) {
+                grid.getSelectionModel().deselectAll();
+            }
+            grid.getView().refresh();
+            me.up('window').updateActions();
+        };
+        if (!allow) {
+            apply(undefined);
+            return;
+        }
+        // the wipe check needs the storages as they are now, a base storage may be new
+        Proxmox.Utils.API2Request({
+            url: '/storage',
+            method: 'GET',
+            failure: () => apply(undefined),
+            success: (response) => apply(response.result.data || []),
+        });
+    },
+
+    // wipeButton completes the config of the Wipe... button
+    wipeControls: function (rescan, wipeButton) {
+        let me = this;
+        return [
+            {
+                xtype: 'button',
+                itemId: 'rescan',
+                text: gettext('Rescan'),
+                iconCls: 'fa fa-search',
+                handler: rescan,
+            },
+            {
+                xtype: 'proxmoxcheckbox',
+                itemId: 'allowWipe',
+                margin: '0 10',
+                hidden: Proxmox.UserName !== 'root@pam',
+                boxLabel: me.allowWipeText,
+                submitValue: false,
+                listeners: {
+                    change: (field, value) => me.setAllowWipe(value),
+                },
+            },
+            {
+                xtype: 'button',
+                itemId: 'wipeButton',
+                text: gettext('Wipe...'),
+                cls: 'pve-dangerous-button',
+                hidden: true,
+                disabled: true,
+                ...wipeButton,
+            },
+        ];
+    },
+
     lunColumns: function () {
         return {
             paths: {
diff --git a/www/manager6/storage/wizard/LunSelect.js b/www/manager6/storage/wizard/LunSelect.js
index 4e138f787..947105f4d 100644
--- a/www/manager6/storage/wizard/LunSelect.js
+++ b/www/manager6/storage/wizard/LunSelect.js
@@ -23,9 +23,14 @@ Ext.define('PVE.storage.wizard.LunSelect', {
     tabTitle: gettext('LUN'),
 
     selectText: gettext('Select an unused LUN for the volume group.'),
+    wipeSelectText: gettext(
+        'Select an unused LUN for the volume group. If a LUN holds data that is no longer needed anywhere and no configured storage uses it for data, you can wipe it first.',
+    ),
     waitText: gettext('Waiting for node "{0}" to list the LUNs of the target...'),
     errorText: gettext('Could not list the LUNs of the target on node "{0}": {1}'),
     selectedText: gettext('The volume group will be created on LUN {0} ({1} {2}, {3}).'),
+    wipeText: gettext('LUN {0} ({1} {2}, {3}) holds data. Press Wipe... to review and wipe it.'),
+    wipedText: gettext('Wipe completed. Select a LUN to continue.'),
     pinnedText: gettext(
         'Press {4} to retry on LUN {0} ({1} {2}, {3}). The wizard keeps this LUN, as the previous attempt may have written to it.',
     ),
@@ -69,6 +74,15 @@ Ext.define('PVE.storage.wizard.LunSelect', {
         return undefined;
     },
 
+    wipeable: function (rec) {
+        let me = this;
+        return (
+            me.allowWipe &&
+            !rec.get('reason') &&
+            !PVE.window.SANLunWipe.reason(rec.data, me.storageConfig)
+        );
+    },
+
     getSelection: function () {
         return this.down('#lunGrid').getSelection()[0]?.data;
     },
@@ -93,10 +107,20 @@ Ext.define('PVE.storage.wizard.LunSelect', {
         let grid = me.down('#lunGrid');
         let rec = grid.getSelection()[0];
         let row = pinned ? wizard.created.lun : rec?.data;
-        let blocked = wizard.busy;
+        let blocked = wizard.busy || !!wizard.wipeOperation;
         grid.setDisabled(!!pinned || blocked || me.scanning || me.scanError);
         me.down('#rescan').setHidden(!!pinned);
         me.down('#rescan').setDisabled(blocked || me.scanning);
+        let optin = me.down('#allowWipe');
+        optin.setHidden(!!pinned || Proxmox.UserName !== 'root@pam');
+        optin.setDisabled(blocked || me.scanning);
+        if (pinned && optin.getValue()) {
+            optin.setValue(false);
+        }
+        me.down('#wipeButton').setHidden(!!pinned || !me.allowWipe);
+        me.down('#wipeButton').setDisabled(
+            blocked || me.scanning || me.scanError || !rec || !me.wipeable(rec),
+        );
         me.down('#footnote').setHidden(!!pinned || !!wizard.targetUse?.reuse);
         let text;
         if (wizard.unconfirmedStorage) {
@@ -108,7 +132,7 @@ Ext.define('PVE.storage.wizard.LunSelect', {
         } else if (me.scanError) {
             text = '';
         } else if (row) {
-            text = me.selectedText;
+            text = rec.get('selectable') ? me.selectedText : me.wipeText;
         } else if (me.activationOutcome && !grid.getStore().getCount()) {
             let outcome = me.activationOutcome;
             text = Ext.String.format(
@@ -122,15 +146,17 @@ Ext.define('PVE.storage.wizard.LunSelect', {
                 me.node,
                 me.initiator || Proxmox.Utils.unknownText,
             );
+        } else if (me.wiped) {
+            text = me.wipedText;
         } else if (
             !grid
                 .getStore()
                 .getRange()
-                .some((r) => r.get('selectable'))
+                .some((r) => r.get('selectable') || me.wipeable(r))
         ) {
             text = Ext.String.format(me.noneText, me.initiator || Proxmox.Utils.unknownText);
         } else {
-            text = me.selectText;
+            text = me.allowWipe ? me.wipeSelectText : me.selectText;
         }
         if (row && !wizard.unconfirmedStorage && !me.scanning && !me.scanError) {
             text = Ext.String.format(
@@ -142,7 +168,7 @@ Ext.define('PVE.storage.wizard.LunSelect', {
                 wizard.actionText(),
             );
         }
-        me.down('#stateLine').setValue(Ext.htmlEncode(text || ''));
+        me.down('#stateLine').setStatus(text);
     },
 
     showScanError: function (errorHtml) {
@@ -197,7 +223,7 @@ Ext.define('PVE.storage.wizard.LunSelect', {
                 value: me.recap,
             },
             {
-                xtype: 'displayfield',
+                xtype: 'pveStorageWizardWipeStatus',
                 itemId: 'stateLine',
             },
             {
@@ -207,10 +233,11 @@ Ext.define('PVE.storage.wizard.LunSelect', {
                 store: store,
                 viewConfig: {
                     trackOver: false,
-                    getRowClass: (rec) => (rec.get('selectable') ? '' : 'x-item-disabled'),
+                    getRowClass: (rec) =>
+                        rec.get('selectable') || me.wipeable(rec) ? '' : 'x-item-disabled',
                 },
                 listeners: {
-                    beforeselect: (grid, rec) => rec.get('selectable'),
+                    beforeselect: (grid, rec) => rec.get('selectable') || me.wipeable(rec),
                     selectionchange: function () {
                         me.selectionGeneration++;
                         me.updateButtons();
@@ -247,9 +274,14 @@ Ext.define('PVE.storage.wizard.LunSelect', {
                                 metaData.tdAttr = `data-qtip="${Ext.htmlEncode(text)}"`;
                                 return text;
                             }
+                            let refusal =
+                                me.allowWipe && !rec.get('selectable')
+                                    ? PVE.window.SANLunWipe.reason(rec.data, me.storageConfig)
+                                    : undefined;
                             let { text, tip } = PVE.SANUtils.san_lun_usage(rec.data, {
                                 // every LUN of the storage carries its attribution
                                 ownStorage: me.storage,
+                                refusal,
                             });
                             metaData.tdAttr = `data-qtip="${Ext.htmlEncode(tip)}"`;
                             return text;
@@ -259,15 +291,20 @@ Ext.define('PVE.storage.wizard.LunSelect', {
             },
         ];
 
-        let controls = [
-            {
-                xtype: 'button',
-                itemId: 'rescan',
-                text: gettext('Rescan'),
-                iconCls: 'fa fa-search',
-                handler: () => me.doRescan(),
+        let controls = me.wipeControls(() => me.doRescan(), {
+            handler: function () {
+                let rec = me.down('#lunGrid').getSelection()[0];
+                if (!rec || !me.wipeable(rec) || me.up('window').created?.baseVolume) {
+                    return;
+                }
+                me.up('window').openWipe(me.node, rec.data, function (wiped) {
+                    if (!me.destroyed) {
+                        me.wiped = wiped;
+                        me.waitForLuns(false);
+                    }
+                });
             },
-        ];
+        });
         me.items.push({ xtype: 'container', layout: 'hbox', margin: '5 0', items: controls });
         me.items.push({ xtype: 'displayfield', itemId: 'footnote', value: me.footnote });
 
@@ -440,6 +477,7 @@ Ext.define('PVE.storage.wizard.LunSelect', {
 
     doRescan: function () {
         let me = this;
+        me.wiped = false;
         me.preselectionConsumed = true;
         me.waitForLuns(false);
     },
diff --git a/www/manager6/storage/wizard/WipeStatus.js b/www/manager6/storage/wizard/WipeStatus.js
new file mode 100644
index 000000000..a79c25aec
--- /dev/null
+++ b/www/manager6/storage/wizard/WipeStatus.js
@@ -0,0 +1,34 @@
+Ext.define('PVE.storage.wizard.WipeStatus', {
+    extend: 'Ext.container.Container',
+    xtype: 'pveStorageWizardWipeStatus',
+    layout: { type: 'hbox', align: 'middle' },
+
+    setStatus: function (text) {
+        let me = this;
+        let wizard = me.up('window');
+        let op = wizard.wipeOperation;
+        let warning =
+            op?.state === 'unknown' ? '<i class="fa fa-exclamation-triangle warning"></i> ' : '';
+        me.down('#text').setValue(warning + Ext.htmlEncode(wizard.wipeStatusText() || text || ''));
+        me.down('#resume').setVisible(!!op?.upid && !wizard.busy);
+        me.down('#clear').setVisible(op?.state === 'unknown' && !op.upid && !wizard.busy);
+    },
+
+    items: [
+        { xtype: 'displayfield', itemId: 'text', flex: 1, value: '' },
+        {
+            xtype: 'button',
+            itemId: 'resume',
+            text: gettext('Check Task Status'),
+            hidden: true,
+            handler: (button) => button.up('window').waitForWipe(),
+        },
+        {
+            xtype: 'button',
+            itemId: 'clear',
+            text: gettext('Dismiss'),
+            hidden: true,
+            handler: (button) => button.up('window').clearWipe(),
+        },
+    ],
+});
diff --git a/www/manager6/storage/wizard/Wizard.js b/www/manager6/storage/wizard/Wizard.js
index 433612b1a..d50291584 100644
--- a/www/manager6/storage/wizard/Wizard.js
+++ b/www/manager6/storage/wizard/Wizard.js
@@ -155,7 +155,7 @@ Ext.define('PVE.storage.wizard.RemoteStorage', {
 
     canSubmit: function () {
         let me = this;
-        if (me.busy || me.unknownVolumeGroup) {
+        if (me.busy || me.wipeOperation || me.unknownVolumeGroup) {
             return false;
         }
         let canSubmit = PVE.storage.wizard.types[me.wizardType].canSubmit;
@@ -171,6 +171,7 @@ Ext.define('PVE.storage.wizard.RemoteStorage', {
             me.lunView &&
             me.targetUse?.reuse &&
             !me.busy &&
+            !me.wipeOperation &&
             !me.unconfirmedStorage &&
             Object.keys(me.created ?? {}).every((key) => key === 'baseStorage')
         );
@@ -202,15 +203,115 @@ Ext.define('PVE.storage.wizard.RemoteStorage', {
         let confirmActive = me.down('#wizcontent').getActiveTab() === me.down('#confirm');
         if (PVE.storage.wizard.types[me.wizardType].canSubmit && confirmActive) {
             me.down('#submit').setDisabled(!me.canSubmit());
-            me.down('#back').setDisabled(!!me.frozenValues || me.busy);
+            me.down('#back').setDisabled(!!me.frozenValues || me.busy || !!me.wipeOperation);
         }
+        me.query('pveStorageWizardWipeStatus').forEach((status) => {
+            if (status !== me.lunView?.down('#stateLine')) {
+                status.setStatus();
+            }
+        });
         me.fireEvent('setupchange', me);
         // after the listeners, so no step can enable an action again
-        if (me.busy || me.unknownVolumeGroup) {
+        if (me.busy || me.wipeOperation || me.unknownVolumeGroup) {
             ['#back', '#next', '#submit'].forEach((id) => me.down(id).disable());
         }
     },
 
+    wipeStatusText: function () {
+        let op = this.wipeOperation;
+        if (!op) {
+            return '';
+        }
+        let t = PVE.window.SANLunWipe.texts(op.lun);
+        if (op.state === 'pending' || op.state === 'waiting') {
+            return Ext.String.format(t.wiping, op.identity, op.node);
+        }
+        if (!op.upid) {
+            return Ext.String.format(t.unknownRequest, op.node);
+        }
+        return Ext.String.format(
+            t.unknownTask,
+            op.lun['iscsi-lun'] ?? op.lun.devpath,
+            'WWID',
+            op.identity,
+        );
+    },
+
+    openWipe: function (node, lun, refresh) {
+        let me = this;
+        if (me.busy || me.wipeOperation) {
+            return;
+        }
+        Ext.create('PVE.window.SANLunWipe', {
+            node,
+            lun,
+            autoShow: true,
+            onSubmit: function (op) {
+                op.refresh = refresh;
+                me.wipeOperation = op;
+                me.setBusy(true, gettext('Wiping...'));
+            },
+            onClose: function (op) {
+                if (me.destroyed || me.wipeOperation !== op) {
+                    return;
+                }
+                if (op.state !== 'unknown') {
+                    delete me.wipeOperation;
+                }
+                me.setBusy(false);
+                refresh(false);
+                me.updateActions();
+            },
+            callback: function (op) {
+                if (me.destroyed || me.wipeOperation !== op) {
+                    return;
+                }
+                me.waitForWipe();
+            },
+        });
+    },
+
+    waitForWipe: function () {
+        let me = this;
+        let op = me.wipeOperation;
+        if (!op?.upid) {
+            return;
+        }
+        op.state = 'waiting';
+        let done = function (success, final, msg) {
+            if (me.destroyed || me.wipeOperation !== op) {
+                return;
+            }
+            if (final) {
+                delete me.wipeOperation;
+            } else {
+                op.state = 'unknown';
+            }
+            op.refresh(success);
+            me.updateActions();
+            if (final && !success) {
+                Ext.Msg.alert(gettext('Error'), msg);
+            }
+        };
+        me.waitForTask(op.node, op.upid, {
+            msg: Ext.htmlEncode(me.wipeStatusText()),
+            texts: { unknown: gettext('Could not determine the task outcome') },
+            success: () => done(true, true),
+            failure: (msg, final) => done(false, final, msg),
+        });
+    },
+
+    clearWipe: function () {
+        let me = this;
+        let op = me.wipeOperation;
+        if (me.busy || op?.state !== 'unknown' || op.upid) {
+            return;
+        }
+        delete me.wipeOperation;
+        op.refresh(false);
+        me.updateActions();
+    },
+
     activateLunView: function (config) {
         let me = this;
         if (me.lunView) {
@@ -843,6 +944,10 @@ Ext.define('PVE.storage.wizard.RemoteStorage', {
         });
 
         me.callParent();
+        let confirmItems = entry.confirmItems?.();
+        if (confirmItems) {
+            me.down('#confirm').add(confirmItems);
+        }
         let back = me.down('#back');
         let backHandler = back.handler;
         back.setHandler(function () {
@@ -857,6 +962,9 @@ Ext.define('PVE.storage.wizard.RemoteStorage', {
             field.on('validitychange', () => me.updateActions());
         });
         me.down('#wizcontent').on('tabchange', () => me.updateActions());
-        me.down('#wizcontent').on('beforetabchange', () => !me.busy && !me.frozenValues);
+        me.down('#wizcontent').on(
+            'beforetabchange',
+            () => !me.busy && !me.wipeOperation && !me.frozenValues,
+        );
     },
 });
-- 
2.47.3




  parent reply	other threads:[~2026-11-06  1:02 UTC|newest]

Thread overview: 77+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-05 23:30 [PATCH v3 proxmox,proxmox-perl-rs,cluster,storage,docs,manager 00/76] SAN: cluster-wide multipath, SAN views and a guided storage setup Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 proxmox 01/76] add proxmox-multipath crate Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 proxmox-perl-rs 02/76] pve-rs: add PVE::RS::Multipath bindings for multipathd status parsing Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 cluster 03/76] cfs: observe the multipath configuration files Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 04/76] storage: distinguish status-only activation in plugins Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 05/76] lvm: find a shared volume group created by another node Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 06/76] storage: bound LVM status commands Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 07/76] iscsi: rate-limit session rescans per target Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 08/76] iscsi: list the LUNs of each session, not of its SCSI host Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 09/76] lvm: refuse to create a volume group on a device that holds data Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 10/76] lvm: refuse a base volume of a target that serves disk images Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 11/76] iscsi: refuse virtual devices other than multipath maps as volumes Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 12/76] diskmanage: collect disk transport type from lsblk Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 13/76] diskmanage: add helper to list multipath devices Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 14/76] diskmanage: qualify NVMe over fabrics transport Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 15/76] disks: list: add include-remote parameter Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 16/76] diskmanage: include iSCSI session devices in disk enumeration Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 17/76] diskmanage: link multipath member disks to their map device Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 18/76] iscsi: factor out session device map from device list Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 19/76] api: scan: add a SAN LUN inventory Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 20/76] pvesm: expose the " Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 21/76] disks: lvm: allow volume groups on SAN LUNs and multipath maps Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 22/76] multipath: add a library around multipathd Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 23/76] diskmanage: collect native NVMe multipath path state Thomas Lamprecht
2026-11-05 23:30 ` [PATCH v3 storage 24/76] api: scan: san-luns: report " Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 25/76] diskmanage: collect local block device users Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 26/76] api: add node-level iSCSI status and maintenance Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 27/76] api: iscsi: add session logout with last-path protection Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 28/76] multipath: add a cluster-wide configuration Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 29/76] iscsi: add an option to manage storage LUNs with multipath Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 30/76] lvm: activate a missing base volume before using the volume group Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 31/76] multipath: apply the configuration on every node Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 32/76] iscsi: share the initiator identity reader Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 33/76] multipath: publish per-node status Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 34/76] api: add endpoints for the cluster-wide multipath configuration Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 35/76] api: multipath: add the cluster-wide health status Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 36/76] multipath: collect identities of non-iSCSI SCSI paths Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 37/76] api: scan: san-luns: report the multipath state of each LUN Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 38/76] api: scan: flag duplicate physical volumes on distinct LUNs Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 39/76] api: disks: recheck local users and retain wipe device references Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 40/76] iscsi: name plain LUNs after their identity link Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 41/76] api: disks: guard SAN wipes with stable identities and storage checks Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 storage 42/76] api: scan: refresh SAN usage with bounded probes Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 docs 43/76] storage: lvm: note the rescan after extending a shared volume group Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 docs 44/76] storage: document the cluster-wide multipath management Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 45/76] api: ceph: osd: keep iSCSI LUNs out of OSD creation Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 46/76] pvestatd: track pending storage status commands across re-exec Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 47/76] ui: storage: factor out the scan picker error mask Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 48/76] ui: storage: nfs: show scan errors and empty results Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 49/76] ui: storage: allow changing the node for NFS/CIFS scans Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 50/76] ui: storage: add guided remote storage wizard with NFS support Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 51/76] ui: storage wizard: add SMB/CIFS support Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 52/76] ui: storage wizard: preserve settings after partial setup Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 53/76] ui: storage wizard: add Fibre Channel shared LVM setup Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 54/76] ui: storage wizard: reconcile unanswered storage creation Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 55/76] ui: storage wizard: add ZFS over iSCSI support Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 56/76] ui: dc: storage: add remote storage wizard entry to the add menu Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 57/76] ui: node: add SAN LUNs panel Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 58/76] ui: san luns: show multipath path state Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 59/76] api: nodes: expose iSCSI status and maintenance Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 60/76] pvenode: add iscsi commands Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 61/76] ui: node: san: add iSCSI target and session management Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 62/76] pvestatd: apply the multipath configuration and report the node status Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 63/76] api: cluster: add the multipath endpoints Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 64/76] ui: dc: add a cluster-wide multipath view Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 65/76] ui: storage: iscsi: add the multipath option Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 66/76] ui: san luns: show multipath state and manage LUN entries Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 67/76] ui: storage wizard: add iSCSI support Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 68/76] ui: storage wizard: iscsi: show cluster initiator names Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 69/76] ui: storage wizard: iscsi: enable multipath for shared LVM Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 70/76] ui: node: san: wipe a LUN that no storage uses Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 71/76] ui: storage wizard: iscsi: reuse existing target connections Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 72/76] ui: storage wizard: fibre channel: enable multipath for a LUN Thomas Lamprecht
2026-11-05 23:31 ` Thomas Lamprecht [this message]
2026-11-05 23:31 ` [PATCH v3 manager 74/76] ui: storage wizard: do not offer LUNs in use for a new volume group Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 75/76] ui: san: show the local users of a LUN in its usage tooltip Thomas Lamprecht
2026-11-05 23:31 ` [PATCH v3 manager 76/76] ui: san: check the other nodes for users before wiping a LUN Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261105233336.2178064-74-t.lamprecht@proxmox.com \
    --to=t.lamprecht@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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