pve-manager/www/manager6/window/Migrate.js
Thomas Lamprecht 685b52f58c ui: window/Migrate: avoid triggering another info request if already in-progress
use the node as key, so that a target switch would still trigger a
new one - else there's a slight chance that a user could get the
check out-of-sync (from another node).

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-05-19 16:52:49 +02:00

433 lines
11 KiB
JavaScript

Ext.define('PVE.window.Migrate', {
extend: 'Ext.window.Window',
vmtype: undefined,
nodename: undefined,
vmid: 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,
},
},
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;
}
},
},
},
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);
view.setTitle(
Ext.String.format('{0} {1} {2}', gettext('Migrate'), vm.get(view.vmtype).commonName, view.vmid),
);
me.lookup('proxmoxHelpButton').setHelpConfig({
onlineHelp: vm.get(view.vmtype).onlineHelp,
});
me.lookup('formPanel').isValid();
},
onTargetChange: function(nodeSelector) {
// Always display the storages of the currently seleceted migration target
this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value);
this.checkMigratePreconditions();
},
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;
}
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(gettext('Error'), 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: 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);
}
if (vm.get('vmtype') === 'qemu') {
me.checkQemuPreconditions(resetMigrationPossible);
} else {
me.checkLxcPreconditions(resetMigrationPossible);
}
me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')];
// 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;
me.fetchingNodeMigrateInfo = false;
} catch (error) {
Ext.Msg.alert(gettext('Error'), error.htmlStatus);
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 = [];
if (migrateStats.allowed_nodes) {
migration.allowedNodes = migrateStats.allowed_nodes;
let target = me.lookup('pveNodeSelector').value;
if (target.length && !migrateStats.allowed_nodes.includes(target)) {
let disallowed = migrateStats.not_allowed_nodes[target];
let missingStorages = disallowed.unavailable_storages.join(', ');
migration.possible = false;
migration.preconditions.push({
text: 'Storage (' + missingStorages + ') not available on selected target. ' +
'Start VM to use live storage migration or select other target node',
severity: 'error',
});
}
}
if (migrateStats.local_resources.length) {
migration.hasLocalResources = true;
if (!migration.overwriteLocalResourceCheck || vm.get('running')) {
migration.possible = false;
migration.preconditions.push({
text: Ext.String.format('Can\'t migrate VM with local resources: {0}',
migrateStats.local_resources.join(', ')),
severity: 'error',
});
} else {
migration.preconditions.push({
text: Ext.String.format('Migrate VM with local resources: {0}. ' +
'This might fail if resources aren\'t available on the target node.',
migrateStats.local_resources.join(', ')),
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')) {
if (migrateStats.running) {
migration.possible = false;
migration.preconditions.push({
text: "Can't live migrate VM with local cloudinit disk, use shared storage instead",
severity: 'error',
});
}
} else {
migration.possible = false;
migration.preconditions.push({
text: "Can't migrate VM with local CD/DVD",
severity: 'error',
});
}
} else {
let size = disk.size ? '(' + Proxmox.Utils.render_size(disk.size) + ')' : '';
migration['with-local-disks'] = 1;
migration.preconditions.push({
text: Ext.String.format('Migration with local disk might take long: {0} {1}', disk.volid, size),
severity: 'warning',
});
}
});
}
vm.set('migration', migration);
},
checkLxcPreconditions: function(resetMigrationPossible) {
let vm = this.getViewModel();
if (vm.get('running')) {
vm.set('migration.mode', 'restart');
}
},
},
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': 'Overwrite local resources unavailable check',
},
bind: {
hidden: '{setLocalResourceCheckboxHidden}',
value: '{migration.overwriteLocalResourceCheck}',
},
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 '<i class="fa fa-exclamation-triangle warning"></i> ';
case 'error':
return '<i class="fa fa-times critical"></i>';
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}',
},
},
],
});