pve-manager/www/manager6/window/Migrate.js
Fabian Ebner 11c17a01b7 Allow setting no target storage and make it default
so the current disk locations can be preserved even if
there are multiple local disks. And users don't have to
manually select the current storage if there is only one
local disk.

Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
2020-06-06 18:49:08 +02:00

438 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.checkMigratePreconditions();
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: function(resetMigrationPossible) {
var me = this,
vm = me.getViewModel(),
migrateStats;
if (vm.get('running')) {
vm.set('migration.mode', 'online');
}
Proxmox.Utils.API2Request({
url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
method: 'GET',
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
success: function(response, options) {
migrateStats = response.result.data;
if (migrateStats.running) {
vm.set('running', true);
}
// Get migration object from viewmodel to prevent
// to many bind callbacks
var migration = vm.get('migration');
if (resetMigrationPossible) migration.possible = true;
migration.preconditions = [];
if (migrateStats.allowed_nodes) {
migration.allowedNodes = migrateStats.allowed_nodes;
var target = me.lookup('pveNodeSelector').value;
if (target.length && !migrateStats.allowed_nodes.includes(target)) {
let disallowed = migrateStats.not_allowed_nodes[target];
let missing_storages = disallowed.unavailable_storages.join(', ');
migration.possible = false;
migration.preconditions.push({
text: 'Storage (' + missing_storages + ') 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 {
return;
}
} else {
migration.possible = false;
migration.preconditions.push({
text: "Can't migrate VM with local CD/DVD",
severity: 'error'
});
}
} else {
var size_string = disk.size ? '(' + PVE.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_string),
severity: 'warning'
});
}
});
}
vm.set('migration', migration);
}
});
},
checkLxcPreconditions: function(resetMigrationPossible) {
var me = this,
vm = me.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: {
type: 'column'
},
items: [
{
xtype: 'container',
columnWidth: 0.5,
items: [{
xtype: 'displayfield',
name: 'source',
fieldLabel: gettext('Source node'),
bind: {
value: '{nodename}'
}
},
{
xtype: 'displayfield',
reference: 'migrationMode',
fieldLabel: gettext('Mode'),
bind: {
value: '{setMigrationMode}'
}
}]
},
{
xtype: 'container',
columnWidth: 0.5,
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: 'use 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}'
}
}
}
],
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}'
}
}
]
});