all lists on 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal