From 0bb7be3f7f309ab0bee91af070e01021388a825b Mon Sep 17 00:00:00 2001 From: upstream Date: Sun, 11 Oct 2026 13:13:26 +0200 Subject: [PATCH] Upstream proxmox-git/pve-manager state of the touched files before a8c37f51fb --- www/manager6/window/Migrate.js | 740 +++++++++++++++++++++++++++++++++ 1 file changed, 740 insertions(+) create mode 100644 www/manager6/window/Migrate.js diff --git a/www/manager6/window/Migrate.js b/www/manager6/window/Migrate.js new file mode 100644 index 0000000..8cac54e --- /dev/null +++ b/www/manager6/window/Migrate.js @@ -0,0 +1,740 @@ +Ext.define('PVE.window.Migrate', { + extend: 'Ext.window.Window', + + vmtype: undefined, + nodename: undefined, + vmid: undefined, + vmname: undefined, + maxHeight: 450, + + viewModel: { + data: { + vmid: undefined, + nodename: undefined, + vmtype: undefined, + running: false, + qemu: { + onlineHelp: 'qm_migration', + commonName: 'VM', + }, + lxc: { + onlineHelp: 'pct_migration', + commonName: 'CT', + }, + migration: { + possible: true, + preconditions: [], + 'with-local-disks': 0, + mode: undefined, + allowedNodes: undefined, + overwriteLocalResourceCheck: false, + hasLocalResources: false, + withConntrackState: true, + bothHaveDbusVmstate: false, + }, + }, + formulas: { + setMigrationMode: function (get) { + if (get('running')) { + if (get('vmtype') === 'qemu') { + return gettext('Online'); + } else { + return gettext('Restart Mode'); + } + } else { + return gettext('Offline'); + } + }, + setStorageselectorHidden: function (get) { + if (get('migration.with-local-disks') && get('running')) { + return false; + } else { + return true; + } + }, + setLocalResourceCheckboxHidden: function (get) { + if ( + get('running') || + !get('migration.hasLocalResources') || + Proxmox.UserName !== 'root@pam' + ) { + return true; + } else { + return false; + } + }, + conntrackStateCheckboxHidden: (get) => + !get('running') || + get('vmtype') !== 'qemu' || + !get('migration.bothHaveDbusVmstate'), + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'panel[reference=formPanel]': { + validityChange: function (panel, isValid) { + this.getViewModel().set('migration.possible', isValid); + this.checkMigratePreconditions(); + }, + }, + }, + + init: function (view) { + var me = this, + vm = view.getViewModel(); + + if (!view.nodename) { + throw 'missing custom view config: nodename'; + } + vm.set('nodename', view.nodename); + + if (!view.vmid) { + throw 'missing custom view config: vmid'; + } + vm.set('vmid', view.vmid); + + if (!view.vmtype) { + throw 'missing custom view config: vmtype'; + } + vm.set('vmtype', view.vmtype); + + let title = Ext.String.format( + '{0} {1} {2}', + gettext('Migrate'), + vm.get(view.vmtype).commonName, + PVE.Utils.getFormattedGuestIdentifier(view.vmid, view.vmname), + ); + view.setTitle(title); + + me.lookup('proxmoxHelpButton').setHelpConfig({ + onlineHelp: vm.get(view.vmtype).onlineHelp, + }); + me.lookup('formPanel').isValid(); + }, + + onTargetChange: function (nodeSelector) { + // Always display the storages of the currently selected migration target + this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value); + this.checkMigratePreconditions(true); + }, + + startMigration: function () { + var me = this, + view = me.getView(), + vm = me.getViewModel(); + + var values = me.lookup('formPanel').getValues(); + var params = { + target: values.target, + }; + + if (vm.get('migration.mode')) { + params[vm.get('migration.mode')] = 1; + } + if (vm.get('migration.with-local-disks')) { + params['with-local-disks'] = 1; + } + //offline migration to a different storage currently might fail at a late stage + //(i.e. after some disks have been moved), so don't expose it yet in the GUI + if (vm.get('migration.with-local-disks') && vm.get('running') && values.targetstorage) { + params.targetstorage = values.targetstorage; + } + + if (vm.get('migration.overwriteLocalResourceCheck')) { + params.force = 1; + } + + if (vm.get('migration.bothHaveDbusVmstate') && vm.get('migration.withConntrackState')) { + params['with-conntrack-state'] = 1; + } + + Proxmox.Utils.API2Request({ + params: params, + url: + '/nodes/' + + vm.get('nodename') + + '/' + + vm.get('vmtype') + + '/' + + vm.get('vmid') + + '/migrate', + waitMsgTarget: view, + method: 'POST', + failure: function (response, opts) { + Ext.Msg.alert(Proxmox.Utils.errorText, response.htmlStatus); + }, + success: function (response, options) { + var upid = response.result.data; + var extraTitle = Ext.String.format( + ' ({0} ---> {1})', + vm.get('nodename'), + params.target, + ); + + Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + extraTitle: extraTitle, + }).show(); + + view.close(); + }, + }); + }, + + checkMigratePreconditions: async function (resetMigrationPossible) { + var me = this, + vm = me.getViewModel(); + + var vmrec = PVE.data.ResourceStore.findRecord( + 'vmid', + vm.get('vmid'), + 0, + false, + false, + true, + ); + if (vmrec && vmrec.data && vmrec.data.running) { + vm.set('running', true); + } + + me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')]; + + if (vm.get('vmtype') === 'qemu') { + await me.checkQemuPreconditions(resetMigrationPossible); + } else { + await me.checkLxcPreconditions(resetMigrationPossible); + } + + // Only allow nodes where the local storage is available in case of offline migration + // where storage migration is not possible + me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes'); + + me.lookup('formPanel').isValid(); + }, + + checkQemuPreconditions: async function (resetMigrationPossible) { + let me = this, + vm = me.getViewModel(), + migrateStats; + + if (vm.get('running')) { + vm.set('migration.mode', 'online'); + } + + try { + if ( + me.fetchingNodeMigrateInfo && + me.fetchingNodeMigrateInfo === vm.get('nodename') + ) { + return; + } + me.fetchingNodeMigrateInfo = vm.get('nodename'); + let { result } = await Proxmox.Async.api2({ + url: `/nodes/${vm.get('nodename')}/${vm.get('vmtype')}/${vm.get('vmid')}/migrate`, + method: 'GET', + }); + migrateStats = result.data; + } catch (error) { + if (error?.result?.status !== 501) { + Ext.Msg.alert(Proxmox.Utils.errorText, error.htmlStatus); + } + me.fetchingNodeMigrateInfo = false; + return; + } + + const target = me.lookup('pveNodeSelector').value; + let targetCapabilities = {}; + + try { + const { result } = await Proxmox.Async.api2({ + url: `/nodes/${target}/capabilities/qemu/migration`, + method: 'GET', + }); + targetCapabilities = result.data; + } catch (err) { + // Only emit a warning in the case the target node does not (yet) support the + // `capabilites/qemu/migration` endpoint and simply treat all features as unsupported. + console.warn(`failed to query /capabilites/qemu/migration on '${target}':`, err); + } + + me.fetchingNodeMigrateInfo = false; + + if (migrateStats.running) { + vm.set('running', true); + } + // Get migration object from viewmodel to prevent to many bind callbacks + let migration = vm.get('migration'); + if (resetMigrationPossible) { + migration.possible = true; + } + migration.preconditions = []; + let disallowed = migrateStats.not_allowed_nodes?.[target] ?? {}; + + if (migrateStats.allowed_nodes && !vm.get('running')) { + migration.allowedNodes = migrateStats.allowed_nodes; + if (target.length && !migrateStats.allowed_nodes.includes(target)) { + if (disallowed.unavailable_storages !== undefined) { + let missingStorages = disallowed.unavailable_storages.join(', '); + const text = Ext.String.format( + gettext( + 'Storage(s) ({0}) not available on selected target. Start VM to use live storage migration or select other target node.', + ), + missingStorages, + ); + + migration.possible = false; + migration.preconditions.push({ text, severity: 'error' }); + } + } + } + + if (disallowed['unavailable-resources'] !== undefined) { + let unavailableResources = disallowed['unavailable-resources'].join(', '); + const text = Ext.String.format( + gettext('Mapped Resources ({0}) not available on selected target.'), + unavailableResources, + ); + + migration.possible = false; + migration.preconditions.push({ text, severity: 'error' }); + } + + let blockingResources = []; + let mappedResources = migrateStats['mapped-resource-info'] ?? {}; + + for (const res of migrateStats.local_resources) { + if (!mappedResources[res]) { + blockingResources.push(res); + } + } + + if (blockingResources.length) { + migration.hasLocalResources = true; + if (!migration.overwriteLocalResourceCheck || vm.get('running')) { + const text = Ext.String.format( + gettext('Cannot migrate VM with local resources: {0}'), + blockingResources.join(', '), + ); + + migration.possible = false; + migration.preconditions.push({ text, severity: 'error' }); + } else { + const text = Ext.String.format( + gettext( + 'Migrating VM with local resources: {0}. This might fail if the resources are not available on the target node.', + ), + blockingResources.join(', '), + ); + + migration.preconditions.push({ text, severity: 'warning' }); + } + } + + if (vm.get('running')) { + let allowed = []; + let notAllowed = []; + for (const [key, resource] of Object.entries(mappedResources)) { + if (resource['live-migration']) { + allowed.push(key); + } else { + notAllowed.push(key); + } + } + if (notAllowed.length > 0) { + const text = Ext.String.format( + gettext('Cannot migrate running VM with mapped resources: {0}'), + notAllowed.join(', '), + ); + + migration.possible = false; + migration.preconditions.push({ text, severity: 'error' }); + } else if (allowed.length > 0) { + const text = Ext.String.format( + gettext( + 'Live-migrating running VM with mapped resources (Experimental): {0}', + ), + allowed.join(', '), + ); + + migration.preconditions.push({ text, severity: 'warning' }); + } + } + + if (migrateStats.local_disks.length) { + migrateStats.local_disks.forEach(function (disk) { + if (disk.cdrom && disk.cdrom === 1) { + if (!disk.volid.includes('vm-' + vm.get('vmid') + '-cloudinit')) { + migration.possible = false; + migration.preconditions.push({ + text: gettext('Cannot migrate VM with local CD/DVD'), + severity: 'error', + }); + } + } else { + let size = disk.size + ? '(' + Proxmox.Utils.render_size(disk.size) + ')' + : ''; + const text = Ext.String.format( + gettext('Migration with local disk might take long: {0} {1}'), + disk.volid, + size, + ); + + migration['with-local-disks'] = 1; + migration.preconditions.push({ text, severity: 'warning' }); + } + }); + } + + migration.bothHaveDbusVmstate = + migrateStats['has-dbus-vmstate'] && targetCapabilities['has-dbus-vmstate']; + if (vm.get('running')) { + if (migration.withConntrackState && !migrateStats['has-dbus-vmstate']) { + migration.preconditions.push({ + text: gettext( + 'Cannot migrate conntrack state, source node is lacking support.', + ), + // user cannot really do anything about this, do not bother with scaring them! + severity: 'info', + }); + } + if (migration.withConntrackState && !targetCapabilities['has-dbus-vmstate']) { + migration.preconditions.push({ + text: gettext( + 'Cannot migrate conntrack state, target node is lacking support. Active network connections might get dropped.', + ), + severity: 'warning', + }); + } + + if (migration.bothHaveDbusVmstate && !migration.withConntrackState) { + migration.preconditions.push({ + text: gettext( + 'Conntrack state migration disabled. Active network connections might get dropped.', + ), + severity: 'warning', + }); + } + } + + let blockingHAResources = disallowed['blocking-ha-resources'] ?? []; + if (blockingHAResources.length) { + migration.possible = false; + + for (const { sid, cause } of blockingHAResources) { + let reasonText; + if (cause === 'resource-affinity') { + reasonText = Ext.String.format( + gettext( + 'HA resource {0} with negative affinity to VM on selected target node', + ), + sid, + ); + } else if (cause === 'node-affinity') { + reasonText = Ext.String.format( + gettext('HA resource {0} is not allowed on the selected target node'), + sid, + ); + } else { + reasonText = Ext.String.format( + gettext('blocking HA resource {0} on selected target node'), + sid, + ); + } + + migration.preconditions.push({ + severity: 'error', + text: Ext.String.format( + gettext('Cannot migrate VM, because {0}.'), + reasonText, + ), + }); + } + } + + let dependentHAResources = migrateStats['dependent-ha-resources']; + if (dependentHAResources !== undefined) { + for (const sid of dependentHAResources) { + const text = Ext.String.format( + gettext( + 'HA resource {0} with positive affinity to VM is also migrated to selected target node.', + ), + sid, + ); + + migration.preconditions.push({ text, severity: 'warning' }); + } + } + + vm.set('migration', migration); + }, + checkLxcPreconditions: async function (resetMigrationPossible) { + let me = this; + let vm = me.getViewModel(); + let migrateStats; + + if (vm.get('running')) { + vm.set('migration.mode', 'restart'); + } + + try { + if ( + me.fetchingNodeMigrateInfo && + me.fetchingNodeMigrateInfo === vm.get('nodename') + ) { + return; + } + me.fetchingNodeMigrateInfo = vm.get('nodename'); + let { result } = await Proxmox.Async.api2({ + url: `/nodes/${vm.get('nodename')}/${vm.get('vmtype')}/${vm.get('vmid')}/migrate`, + method: 'GET', + }); + migrateStats = result.data; + me.fetchingNodeMigrateInfo = false; + } catch (error) { + if (error?.result?.status !== 501) { + Ext.Msg.alert(Proxmox.Utils.errorText, error.htmlStatus); + } + me.fetchingNodeMigrateInfo = false; + return; + } + + if (migrateStats.running) { + vm.set('running', true); + } + + // Get migration object from viewmodel to prevent to many bind callbacks + let migration = vm.get('migration'); + if (resetMigrationPossible) { + migration.possible = true; + } + migration.preconditions = []; + let targetNode = me.lookup('pveNodeSelector').value; + let disallowed = migrateStats['not-allowed-nodes']?.[targetNode] ?? {}; + + let blockingHAResources = disallowed['blocking-ha-resources'] ?? []; + if (blockingHAResources.length) { + migration.possible = false; + + for (const { sid, cause } of blockingHAResources) { + let reasonText; + if (cause === 'resource-affinity') { + reasonText = Ext.String.format( + gettext( + 'HA resource {0} with negative affinity to container on selected target node', + ), + sid, + ); + } else if (cause === 'node-affinity') { + reasonText = Ext.String.format( + gettext('HA resource {0} is not allowed on the selected target node'), + sid, + ); + } else { + reasonText = Ext.String.format( + gettext('blocking HA resource {0} on selected target node'), + sid, + ); + } + + migration.preconditions.push({ + severity: 'error', + text: Ext.String.format( + gettext('Cannot migrate container, because {0}.'), + reasonText, + ), + }); + } + } + + let dependentHAResources = migrateStats['dependent-ha-resources']; + if (dependentHAResources !== undefined) { + for (const sid of dependentHAResources) { + const text = Ext.String.format( + gettext( + 'HA resource {0} with positive affinity to container is also migrated to selected target node.', + ), + sid, + ); + + migration.preconditions.push({ text, severity: 'warning' }); + } + } + + vm.set('migration', migration); + }, + }, + + width: 600, + modal: true, + layout: { + type: 'vbox', + align: 'stretch', + }, + border: false, + items: [ + { + xtype: 'form', + reference: 'formPanel', + bodyPadding: 10, + border: false, + layout: 'hbox', + items: [ + { + xtype: 'container', + flex: 1, + items: [ + { + xtype: 'displayfield', + name: 'source', + fieldLabel: gettext('Source node'), + bind: { + value: '{nodename}', + }, + }, + { + xtype: 'displayfield', + reference: 'migrationMode', + fieldLabel: gettext('Mode'), + bind: { + value: '{setMigrationMode}', + }, + }, + ], + }, + { + xtype: 'container', + flex: 1, + items: [ + { + xtype: 'pveNodeSelector', + reference: 'pveNodeSelector', + name: 'target', + fieldLabel: gettext('Target node'), + allowBlank: false, + disallowedNodes: undefined, + onlineValidator: true, + listeners: { + change: 'onTargetChange', + }, + }, + { + xtype: 'pveStorageSelector', + reference: 'pveDiskStorageSelector', + name: 'targetstorage', + fieldLabel: gettext('Target storage'), + storageContent: 'images', + allowBlank: true, + autoSelect: false, + emptyText: gettext('Current layout'), + bind: { + hidden: '{setStorageselectorHidden}', + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'overwriteLocalResourceCheck', + fieldLabel: gettext('Force'), + autoEl: { + tag: 'div', + 'data-qtip': gettext('Overwrite local resources unavailable check'), + }, + bind: { + hidden: '{setLocalResourceCheckboxHidden}', + value: '{migration.overwriteLocalResourceCheck}', + }, + listeners: { + change: { + fn: 'checkMigratePreconditions', + extraArg: true, + }, + }, + }, + { + xtype: 'proxmoxcheckbox', + name: 'withConntrackState', + // TRANSLATORS: See https://www.kernel.org/doc/html/next/networking/netlink_spec/conntrack.html + fieldLabel: gettext('Conntrack state'), + autoEl: { + tag: 'div', + 'data-qtip': gettext( + 'Enables live migration of conntrack entries for this VM.', + ), + }, + bind: { + hidden: '{conntrackStateCheckboxHidden}', + value: '{migration.withConntrackState}', + }, + listeners: { + change: { + fn: 'checkMigratePreconditions', + extraArg: true, + }, + }, + }, + ], + }, + ], + }, + { + xtype: 'gridpanel', + reference: 'preconditionGrid', + selectable: false, + flex: 1, + columns: [ + { + text: '', + dataIndex: 'severity', + renderer: function (v) { + switch (v) { + case 'warning': + return ' '; + case 'error': + return ''; + case 'info': + return ''; + default: + return v; + } + }, + width: 35, + }, + { + text: 'Info', + dataIndex: 'text', + cellWrap: true, + flex: 1, + }, + ], + bind: { + hidden: '{!migration.preconditions.length}', + store: { + fields: ['severity', 'text'], + data: '{migration.preconditions}', + sorters: 'text', + }, + }, + }, + ], + buttons: [ + { + xtype: 'proxmoxHelpButton', + reference: 'proxmoxHelpButton', + onlineHelp: 'pct_migration', + listenToGlobalEvent: false, + hidden: false, + }, + '->', + { + xtype: 'button', + reference: 'submitButton', + text: gettext('Migrate'), + handler: 'startMigration', + bind: { + disabled: '{!migration.possible}', + }, + }, + ], +});