Ext.define('PVE.window.Migrate', {
extend: 'Ext.window.Window',
- resizable: false,
-
- migrate: function(target, online) {
- var me = this;
- var params = {
- target: target
- };
-
- if (me.vmtype === 'qemu') {
- params.online = online;
- } else {
- params.restart = online;
- }
+ vmtype: undefined,
+ nodename: undefined,
+ vmid: undefined,
+ maxHeight: 450,
- PVE.Utils.API2Request({
- params: params,
- url: '/nodes/' + me.nodename + '/' + me.vmtype + '/' + me.vmid + "/migrate",
- waitMsgTarget: me,
- method: 'POST',
- failure: function(response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ viewModel: {
+ data: {
+ vmid: undefined,
+ nodename: undefined,
+ vmtype: undefined,
+ running: false,
+ qemu: {
+ onlineHelp: 'qm_migration',
+ commonName: 'VM'
+ },
+ lxc: {
+ onlineHelp: 'pct_migration',
+ commonName: 'CT'
},
- success: function(response, options) {
- var upid = response.result.data;
+ migration: {
+ possible: true,
+ preconditions: [],
+ 'with-local-disks': 0,
+ mode: undefined,
+ allowedNodes: undefined,
+ overwriteLocalResourceCheck: false,
+ hasLocalResources: false
+ }
+
+ },
- var win = Ext.create('PVE.window.TaskViewer', {
- upid: upid
- });
- win.show();
- me.close();
+ 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;
+ }
}
- });
+ }
},
- initComponent : function() {
- var me = this;
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'panel[reference=formPanel]': {
+ validityChange: function(panel, isValid) {
+ this.getViewModel().set('migration.possible', isValid);
+ this.checkMigratePreconditions();
+ }
+ }
+ },
- if (!me.nodename) {
- throw "no node name specified";
- }
+ init: function(view) {
+ var me = this,
+ vm = view.getViewModel();
- if (!me.vmid) {
- throw "no VM ID specified";
- }
+ 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);
- if (!me.vmtype) {
- throw "no VM type specified";
- }
- var running = false;
- var vmrec = PVE.data.ResourceStore.findRecord('vmid', me.vmid,
- 0, false, false, true);
- if (vmrec && vmrec.data && vmrec.data.running) {
- running = true;
+ 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');
+ }
}
- me.formPanel = Ext.create('Ext.form.Panel', {
+
+ },
+
+ width: 600,
+ modal: true,
+ layout: {
+ type: 'vbox',
+ align: 'stretch'
+ },
+ border: false,
+ items: [
+ {
+ xtype: 'form',
+ reference: 'formPanel',
bodyPadding: 10,
border: false,
- fieldDefaults: {
- labelWidth: 100,
- anchor: '100%'
- },
+ layout: 'hbox',
items: [
{
- xtype: 'pveNodeSelector',
- name: 'target',
- fieldLabel: gettext('Target node'),
- allowBlank: false,
- disallowedNodes: [me.nodename],
- onlineValidator: true
+ 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: 'pvecheckbox',
- name: 'online',
- uncheckedValue: 0,
- defaultValue: 0,
- checked: running,
- fieldLabel: me.vtype === 'qemu' ? gettext('Online') : gettext('Restart Mode')
+ 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}
+ }
+ }]
}
]
- });
-
- var form = me.formPanel.getForm();
+ },
+ {
+ 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',
+ }
+ }
+ }
- var submitBtn = Ext.create('Ext.Button', {
+ ],
+ buttons: [
+ {
+ xtype: 'proxmoxHelpButton',
+ reference: 'proxmoxHelpButton',
+ onlineHelp: 'pct_migration',
+ listenToGlobalEvent: false,
+ hidden: false
+ },
+ '->',
+ {
+ xtype: 'button',
+ reference: 'submitButton',
text: gettext('Migrate'),
- handler: function() {
- var values = form.getValues();
- me.migrate(values.target, values.online);
- }
- });
-
- var helpConfig;
- // fixme:
- // the onlinehelp parser needs
- // that every id is explicitely written
- // can we do this better?
- if (me.vmtype === 'qemu') {
- helpConfig = {
- onlineHelp: 'qm_migration',
- listenToGlobalEvent: false,
- hidden: false
- };
- } else {
- helpConfig = {
- onlineHelp: 'pct_migration',
- listenToGlobalEvent: false,
- hidden: false
- };
+ handler: 'startMigration',
+ bind: {
+ disabled: '{!migration.possible}'
+ }
}
-
- var helpBtn = Ext.create('PVE.button.Help', helpConfig);
-
- Ext.apply(me, {
- title: gettext('Migrate') + ((me.vmtype === 'qemu')?' VM ':' CT ') + me.vmid,
- width: 350,
- modal: true,
- layout: 'auto',
- border: false,
- items: [ me.formPanel ],
- buttons: [ helpBtn, '->', submitBtn ]
- });
-
- me.callParent();
- }
+ ]
});