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
next prev 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