]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/window/Migrate.js
ui: backup restore: validate name
[pve-manager.git] / www / manager6 / window / Migrate.js
1 Ext.define('PVE.window.Migrate', {
2 extend: 'Ext.window.Window',
3
4 vmtype: undefined,
5 nodename: undefined,
6 vmid: undefined,
7 maxHeight: 450,
8
9 viewModel: {
10 data: {
11 vmid: undefined,
12 nodename: undefined,
13 vmtype: undefined,
14 running: false,
15 qemu: {
16 onlineHelp: 'qm_migration',
17 commonName: 'VM',
18 },
19 lxc: {
20 onlineHelp: 'pct_migration',
21 commonName: 'CT',
22 },
23 migration: {
24 possible: true,
25 preconditions: [],
26 'with-local-disks': 0,
27 mode: undefined,
28 allowedNodes: undefined,
29 overwriteLocalResourceCheck: false,
30 hasLocalResources: false,
31 },
32
33 },
34
35 formulas: {
36 setMigrationMode: function(get) {
37 if (get('running')) {
38 if (get('vmtype') === 'qemu') {
39 return gettext('Online');
40 } else {
41 return gettext('Restart Mode');
42 }
43 } else {
44 return gettext('Offline');
45 }
46 },
47 setStorageselectorHidden: function(get) {
48 if (get('migration.with-local-disks') && get('running')) {
49 return false;
50 } else {
51 return true;
52 }
53 },
54 setLocalResourceCheckboxHidden: function(get) {
55 if (get('running') || !get('migration.hasLocalResources') ||
56 Proxmox.UserName !== 'root@pam') {
57 return true;
58 } else {
59 return false;
60 }
61 },
62 },
63 },
64
65 controller: {
66 xclass: 'Ext.app.ViewController',
67 control: {
68 'panel[reference=formPanel]': {
69 validityChange: function(panel, isValid) {
70 this.getViewModel().set('migration.possible', isValid);
71 this.checkMigratePreconditions();
72 },
73 },
74 },
75
76 init: function(view) {
77 var me = this,
78 vm = view.getViewModel();
79
80 if (!view.nodename) {
81 throw "missing custom view config: nodename";
82 }
83 vm.set('nodename', view.nodename);
84
85 if (!view.vmid) {
86 throw "missing custom view config: vmid";
87 }
88 vm.set('vmid', view.vmid);
89
90 if (!view.vmtype) {
91 throw "missing custom view config: vmtype";
92 }
93 vm.set('vmtype', view.vmtype);
94
95 view.setTitle(
96 Ext.String.format('{0} {1} {2}', gettext('Migrate'), vm.get(view.vmtype).commonName, view.vmid),
97 );
98 me.lookup('proxmoxHelpButton').setHelpConfig({
99 onlineHelp: vm.get(view.vmtype).onlineHelp,
100 });
101 me.lookup('formPanel').isValid();
102 },
103
104 onTargetChange: function(nodeSelector) {
105 // Always display the storages of the currently seleceted migration target
106 this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value);
107 this.checkMigratePreconditions();
108 },
109
110 startMigration: function() {
111 var me = this,
112 view = me.getView(),
113 vm = me.getViewModel();
114
115 var values = me.lookup('formPanel').getValues();
116 var params = {
117 target: values.target,
118 };
119
120 if (vm.get('migration.mode')) {
121 params[vm.get('migration.mode')] = 1;
122 }
123 if (vm.get('migration.with-local-disks')) {
124 params['with-local-disks'] = 1;
125 }
126 //offline migration to a different storage currently might fail at a late stage
127 //(i.e. after some disks have been moved), so don't expose it yet in the GUI
128 if (vm.get('migration.with-local-disks') && vm.get('running') && values.targetstorage) {
129 params.targetstorage = values.targetstorage;
130 }
131
132 if (vm.get('migration.overwriteLocalResourceCheck')) {
133 params.force = 1;
134 }
135
136 Proxmox.Utils.API2Request({
137 params: params,
138 url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
139 waitMsgTarget: view,
140 method: 'POST',
141 failure: function(response, opts) {
142 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
143 },
144 success: function(response, options) {
145 var upid = response.result.data;
146 var extraTitle = Ext.String.format(' ({0} ---> {1})', vm.get('nodename'), params.target);
147
148 Ext.create('Proxmox.window.TaskViewer', {
149 upid: upid,
150 extraTitle: extraTitle,
151 }).show();
152
153 view.close();
154 },
155 });
156 },
157
158 checkMigratePreconditions: function(resetMigrationPossible) {
159 var me = this,
160 vm = me.getViewModel();
161
162 var vmrec = PVE.data.ResourceStore.findRecord('vmid', vm.get('vmid'),
163 0, false, false, true);
164 if (vmrec && vmrec.data && vmrec.data.running) {
165 vm.set('running', true);
166 }
167
168 if (vm.get('vmtype') === 'qemu') {
169 me.checkQemuPreconditions(resetMigrationPossible);
170 } else {
171 me.checkLxcPreconditions(resetMigrationPossible);
172 }
173 me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')];
174
175 // Only allow nodes where the local storage is available in case of offline migration
176 // where storage migration is not possible
177 me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes');
178
179 me.lookup('formPanel').isValid();
180 },
181
182 checkQemuPreconditions: async function(resetMigrationPossible) {
183 let me = this,
184 vm = me.getViewModel(),
185 migrateStats;
186
187 if (vm.get('running')) {
188 vm.set('migration.mode', 'online');
189 }
190
191 try {
192 if (me.fetchingNodeMigrateInfo && me.fetchingNodeMigrateInfo === vm.get('nodename')) {
193 return;
194 }
195 me.fetchingNodeMigrateInfo = vm.get('nodename');
196 let { result } = await Proxmox.Async.api2({
197 url: `/nodes/${vm.get('nodename')}/${vm.get('vmtype')}/${vm.get('vmid')}/migrate`,
198 method: 'GET',
199 });
200 migrateStats = result.data;
201 me.fetchingNodeMigrateInfo = false;
202 } catch (error) {
203 Ext.Msg.alert(gettext('Error'), error.htmlStatus);
204 return;
205 }
206
207 if (migrateStats.running) {
208 vm.set('running', true);
209 }
210 // Get migration object from viewmodel to prevent to many bind callbacks
211 let migration = vm.get('migration');
212 if (resetMigrationPossible) {
213 migration.possible = true;
214 }
215 migration.preconditions = [];
216
217 if (migrateStats.allowed_nodes) {
218 migration.allowedNodes = migrateStats.allowed_nodes;
219 let target = me.lookup('pveNodeSelector').value;
220 if (target.length && !migrateStats.allowed_nodes.includes(target)) {
221 let disallowed = migrateStats.not_allowed_nodes[target];
222 let missingStorages = disallowed.unavailable_storages.join(', ');
223
224 migration.possible = false;
225 migration.preconditions.push({
226 text: 'Storage (' + missingStorages + ') not available on selected target. ' +
227 'Start VM to use live storage migration or select other target node',
228 severity: 'error',
229 });
230 }
231 }
232
233 if (migrateStats.local_resources.length) {
234 migration.hasLocalResources = true;
235 if (!migration.overwriteLocalResourceCheck || vm.get('running')) {
236 migration.possible = false;
237 migration.preconditions.push({
238 text: Ext.String.format('Can\'t migrate VM with local resources: {0}',
239 migrateStats.local_resources.join(', ')),
240 severity: 'error',
241 });
242 } else {
243 migration.preconditions.push({
244 text: Ext.String.format('Migrate VM with local resources: {0}. ' +
245 'This might fail if resources aren\'t available on the target node.',
246 migrateStats.local_resources.join(', ')),
247 severity: 'warning',
248 });
249 }
250 }
251
252 if (migrateStats.local_disks.length) {
253 migrateStats.local_disks.forEach(function(disk) {
254 if (disk.cdrom && disk.cdrom === 1) {
255 if (!disk.volid.includes('vm-' + vm.get('vmid') + '-cloudinit')) {
256 migration.possible = false;
257 migration.preconditions.push({
258 text: "Can't migrate VM with local CD/DVD",
259 severity: 'error',
260 });
261 }
262 } else {
263 let size = disk.size ? '(' + Proxmox.Utils.render_size(disk.size) + ')' : '';
264 migration['with-local-disks'] = 1;
265 migration.preconditions.push({
266 text: Ext.String.format('Migration with local disk might take long: {0} {1}', disk.volid, size),
267 severity: 'warning',
268 });
269 }
270 });
271 }
272
273 vm.set('migration', migration);
274 },
275 checkLxcPreconditions: function(resetMigrationPossible) {
276 let vm = this.getViewModel();
277 if (vm.get('running')) {
278 vm.set('migration.mode', 'restart');
279 }
280 },
281 },
282
283 width: 600,
284 modal: true,
285 layout: {
286 type: 'vbox',
287 align: 'stretch',
288 },
289 border: false,
290 items: [
291 {
292 xtype: 'form',
293 reference: 'formPanel',
294 bodyPadding: 10,
295 border: false,
296 layout: 'hbox',
297 items: [
298 {
299 xtype: 'container',
300 flex: 1,
301 items: [{
302 xtype: 'displayfield',
303 name: 'source',
304 fieldLabel: gettext('Source node'),
305 bind: {
306 value: '{nodename}',
307 },
308 },
309 {
310 xtype: 'displayfield',
311 reference: 'migrationMode',
312 fieldLabel: gettext('Mode'),
313 bind: {
314 value: '{setMigrationMode}',
315 },
316 }],
317 },
318 {
319 xtype: 'container',
320 flex: 1,
321 items: [{
322 xtype: 'pveNodeSelector',
323 reference: 'pveNodeSelector',
324 name: 'target',
325 fieldLabel: gettext('Target node'),
326 allowBlank: false,
327 disallowedNodes: undefined,
328 onlineValidator: true,
329 listeners: {
330 change: 'onTargetChange',
331 },
332 },
333 {
334 xtype: 'pveStorageSelector',
335 reference: 'pveDiskStorageSelector',
336 name: 'targetstorage',
337 fieldLabel: gettext('Target storage'),
338 storageContent: 'images',
339 allowBlank: true,
340 autoSelect: false,
341 emptyText: gettext('Current layout'),
342 bind: {
343 hidden: '{setStorageselectorHidden}',
344 },
345 },
346 {
347 xtype: 'proxmoxcheckbox',
348 name: 'overwriteLocalResourceCheck',
349 fieldLabel: gettext('Force'),
350 autoEl: {
351 tag: 'div',
352 'data-qtip': 'Overwrite local resources unavailable check',
353 },
354 bind: {
355 hidden: '{setLocalResourceCheckboxHidden}',
356 value: '{migration.overwriteLocalResourceCheck}',
357 },
358 listeners: {
359 change: {
360 fn: 'checkMigratePreconditions',
361 extraArg: true,
362 },
363 },
364 }],
365 },
366 ],
367 },
368 {
369 xtype: 'gridpanel',
370 reference: 'preconditionGrid',
371 selectable: false,
372 flex: 1,
373 columns: [{
374 text: '',
375 dataIndex: 'severity',
376 renderer: function(v) {
377 switch (v) {
378 case 'warning':
379 return '<i class="fa fa-exclamation-triangle warning"></i> ';
380 case 'error':
381 return '<i class="fa fa-times critical"></i>';
382 default:
383 return v;
384 }
385 },
386 width: 35,
387 },
388 {
389 text: 'Info',
390 dataIndex: 'text',
391 cellWrap: true,
392 flex: 1,
393 }],
394 bind: {
395 hidden: '{!migration.preconditions.length}',
396 store: {
397 fields: ['severity', 'text'],
398 data: '{migration.preconditions}',
399 sorters: 'text',
400 },
401 },
402 },
403
404 ],
405 buttons: [
406 {
407 xtype: 'proxmoxHelpButton',
408 reference: 'proxmoxHelpButton',
409 onlineHelp: 'pct_migration',
410 listenToGlobalEvent: false,
411 hidden: false,
412 },
413 '->',
414 {
415 xtype: 'button',
416 reference: 'submitButton',
417 text: gettext('Migrate'),
418 handler: 'startMigration',
419 bind: {
420 disabled: '{!migration.possible}',
421 },
422 },
423 ],
424 });