[pve-devel] [PATCH v2 manager] ui: migrate: refactor migrate window & add migration with local disks

Tim Marx t.marx at proxmox.com
Fri Jun 14 14:35:36 CEST 2019


This patch depends on:
qemu-server: e1f0fbf4448b374eb9a19502aee565adb5be7ec0

This patch refactors the migrate ui to incoperate the viewmodel approach
which should help if we need to add functionality in future iterations.
Additionally it is now possible to migrate with local disks.

Signed-off-by: Tim Marx <t.marx at proxmox.com>
---
changes since v1:
* added error message why seleceted node is not allowed
* layout change 2 columns

 www/manager6/window/Migrate.js | 418 ++++++++++++++++++++++++++++++-----------
 1 file changed, 309 insertions(+), 109 deletions(-)

diff --git a/www/manager6/window/Migrate.js b/www/manager6/window/Migrate.js
index 9395a97f..3a305f32 100644
--- a/www/manager6/window/Migrate.js
+++ b/www/manager6/window/Migrate.js
@@ -1,94 +1,341 @@
+/*jslint confusion: true*/
 Ext.define('PVE.window.Migrate', {
     extend: 'Ext.window.Window',

-    config: {
-	vmtype: undefined,
-	nodename: undefined,
-	vmid: undefined
+
+    vmtype: undefined,
+    nodename: undefined,
+    vmid: undefined,
+
+    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
+	    }
+
+	},
+
+	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;
+		    }
+	    }
+	}
     },
- // private, used to store the migration mode after checking if the guest runs
-    liveMode: undefined,

     controller: {
 	xclass: 'Ext.app.ViewController',
 	control: {
 	    'panel[reference=formPanel]': {
 		validityChange: function(panel, isValid) {
-		    this.lookup('submitButton').setDisabled(!isValid);
+		    this.getViewModel().set('migration.possible', isValid);
+		    this.checkMigratePreconditions();
 		}
-	    },
-	    'button[reference=submitButton]': {
-		click: function() {
-		    var me = this;
-		    var view = me.getView();
-
-		    var values = me.lookup('formPanel').getValues();
-		    var params = {
-			target: values.target
-		    };
-
-		    if (view.liveMode) {
-			params[view.liveMode] = 1;
+	    }
+	},
+
+	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;
+	    }
+	    //only submit targetstorage if vm is running, storage migration to different storage is only possible online
+	    if (vm.get('migration.with-local-disks') && vm.get('running')) {
+		params.targetstorage = values.targetstorage;
+	    }
+
+	    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() {
+	    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();
+	    } else {
+		me.checkLxcPreconditions();
+	    }
+	    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() {
+	    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');
+		    migration.preconditions = [];
+
+		    if (migrateStats.allowed_nodes) {
+			migration.allowedNodes = migrateStats.allowed_nodes;

-		    Proxmox.Utils.API2Request({
-			params: params,
-			url: '/nodes/' + view.nodename + '/' + view.vmtype + '/' + view.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})', view.nodename, params.target);
-
-			    Ext.create('Proxmox.window.TaskViewer', {
-				upid: upid,
-				extraTitle: extraTitle
-			    }).show();
-
-			    view.close();
+			if (!migrateStats.allowed_nodes.includes(me.lookup('pveNodeSelector').value)) {
+			    migration.possible = false;
+			    migration.preconditions.push({
+				text: 'Local storage not available on selected Node, start VM to use live storage migration or select other target node',
+				icon: '<i class="fa fa-times critical"></i>'
+			    });
 			}
-		    });
+		    }
+
+		    if (migrateStats.local_resources.length) {
+			migration.possible = false;
+			migration.preconditions.push({
+			    text: 'Can\'t migrate VM with local resources: '+ migrateStats.local_resources.join(', '),
+			    icon: '<i class="fa fa-times critical"></i>'
+			});
+		    }
+
+		    if (migrateStats.local_disks.length) {
+
+			migrateStats.local_disks.forEach(function (disk) {
+			    if (disk.cdrom && disk.cdrom === 1) {
+				migration.possible = false;
+				migration.preconditions.push({
+				    text:'Can\'t migrate VM with local CD/DVD',
+				    icon: '<i class="fa fa-times critical"></i>'
+				});
+
+			    } else if (!disk.referenced_in_config) {
+				migration.possible = false;
+				migration.preconditions.push({
+				    text: 'Found not referenced/unused disk via storage: '+ disk.volid,
+				    icon: '<i class="fa fa-times critical"></i>'
+				});
+			    } else {
+				migration['with-local-disks'] = 1;
+				migration.preconditions.push({
+				    text:'Migration with local disk might take long: '+ disk.volid,
+				    icon: '<i class="fa fa-exclamation-triangle warning"></i>'
+				});
+			    }
+			});
+
+		    }
+
+		    vm.set('migration', migration);
+
 		}
+	    });
+	},
+	checkLxcPreconditions: function() {
+	    var me = this,
+		vm = me.getViewModel();
+	    if (vm.get('running')) {
+		vm.set('migration.mode', 'restart');
 	    }
 	}
+
+
     },

-    width: 350,
+    width: 700,
     modal: true,
-    layout: 'auto',
+    layout: {
+	type: 'vbox',
+	align: 'stretch'
+    },
     border: false,
-    resizable: false,
     items: [
 	{
 	    xtype: 'form',
 	    reference: 'formPanel',
 	    bodyPadding: 10,
 	    border: false,
-	    fieldDefaults: {
-		labelWidth: 100,
-		anchor: '100%'
+	    layout: {
+		type: 'column'
 	    },
 	    items: [
 		{
-		    xtype: 'pveNodeSelector',
-		    reference: 'pveNodeSelector',
-		    name: 'target',
-		    fieldLabel: gettext('Target node'),
-		    allowBlank: false,
-		    disallowedNodes: undefined,
-		    onlineValidator: true
+		    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: 'displayfield',
+			reference: 'migrationMode',
+			fieldLabel: gettext('Mode'),
+			bind: {
+			    value: '{setMigrationMode}'
+			}
+		    }
+		    ]
 		},
 		{
-		    xtype: 'displayfield',
-		    reference: 'migrationMode',
-		    fieldLabel: gettext('Mode'),
-		    value: gettext('Offline')
+		    xtype: 'container',
+		    columnWidth: 0.5,
+		    items: [
+			{
+				xtype: 'pveStorageSelector',
+				reference: 'pveDiskStorageSelector',
+				name: 'targetstorage',
+				fieldLabel: gettext('Target Storage'),
+				storageContent: 'images',
+				bind: {
+				    hidden: '{setStorageselectorHidden}'
+				}
+			}
+		    ]
+		}
+	    ]
+	},
+	{
+	    xtype: 'gridpanel',
+	    reference: 'preconditionGrid',
+	    flex: 1,
+	    columns: [
+		{text: 'Severity', dataIndex: 'icon', width: 80},
+		{text: 'Info',  dataIndex: 'text', flex: 1}
+	    ],
+	    bind: {
+		hidden: '{!migration.preconditions.length}',
+		store: {
+		    fields: ['icon','text'],
+		    data: '{migration.preconditions}'
 		}
-		]
+	    }
 	}
+
     ],
     buttons: [
 	{
@@ -102,58 +349,11 @@ Ext.define('PVE.window.Migrate', {
 	{
 	    xtype: 'button',
 	    reference: 'submitButton',
-	    text: gettext('Migrate')
-	}
-    ],
-
-    initComponent : function() {
-	var me = this;
-
-	if (!me.nodename) {
-	    throw "no node name specified";
-	}
-
-	if (!me.vmid) {
-	    throw "no VM ID specified";
-	}
-
-	if (!me.vmtype) {
-	    throw "no VM type specified";
-	}
-
-	me.callParent();
-
-	var title = gettext('Migrate') + (' CT ') + me.vmid;
-	me.liveMode = 'restart';
-
-	if (me.vmtype === 'qemu') {
-	    me.lookup('proxmoxHelpButton').setHelpConfig({
-		onlineHelp: 'qm_migration'
-	    });
-	    title = gettext('Migrate') + (' VM ') + me.vmid;
-	    me.liveMode = 'online';
-	}
-
-	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;
-	}
-
-	if (running) {
-	    var displayField = me.lookup('migrationMode');
-	    if (me.vmtype === 'qemu') {
-		displayField.setValue(gettext('Online'));
-		me.liveMode = 'online';
-	    } else {
-		displayField.setValue(gettext('Restart Mode'));
-		me.liveMode = 'restart';
+	    text: gettext('Migrate'),
+	    handler: 'startMigration',
+	    bind: {
+		disabled: '{!migration.possible}'
 	    }
 	}
-
-	me.setTitle(title);
-	me.lookup('pveNodeSelector').disallowedNodes = [me.nodename];
-	me.lookup('formPanel').isValid();
-    }
+    ]
 });
\ No newline at end of file
--
2.11.0




More information about the pve-devel mailing list