]>
Commit | Line | Data |
---|---|---|
04c32bed DM |
1 | Ext.define('PVE.window.Migrate', { |
2 | extend: 'Ext.window.Window', | |
3 | ||
9706707d TM |
4 | vmtype: undefined, |
5 | nodename: undefined, | |
6 | vmid: undefined, | |
8b4987e5 | 7 | maxHeight: 450, |
9706707d TM |
8 | |
9 | viewModel: { | |
10 | data: { | |
11 | vmid: undefined, | |
12 | nodename: undefined, | |
13 | vmtype: undefined, | |
14 | running: false, | |
15 | qemu: { | |
16 | onlineHelp: 'qm_migration', | |
f6710aac | 17 | commonName: 'VM', |
9706707d TM |
18 | }, |
19 | lxc: { | |
20 | onlineHelp: 'pct_migration', | |
f6710aac | 21 | commonName: 'CT', |
9706707d TM |
22 | }, |
23 | migration: { | |
24 | possible: true, | |
25 | preconditions: [], | |
26 | 'with-local-disks': 0, | |
27 | mode: undefined, | |
4ddaf290 TM |
28 | allowedNodes: undefined, |
29 | overwriteLocalResourceCheck: false, | |
f6710aac TL |
30 | hasLocalResources: false, |
31 | }, | |
9706707d TM |
32 | |
33 | }, | |
34 | ||
35 | formulas: { | |
36 | setMigrationMode: function(get) { | |
8058410f | 37 | if (get('running')) { |
9706707d TM |
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 | } | |
4ddaf290 TM |
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 | } | |
f6710aac TL |
61 | }, |
62 | }, | |
2df7ea9f | 63 | }, |
2df7ea9f EK |
64 | |
65 | controller: { | |
66 | xclass: 'Ext.app.ViewController', | |
67 | control: { | |
68 | 'panel[reference=formPanel]': { | |
69 | validityChange: function(panel, isValid) { | |
9706707d TM |
70 | this.getViewModel().set('migration.possible', isValid); |
71 | this.checkMigratePreconditions(); | |
f6710aac TL |
72 | }, |
73 | }, | |
9706707d TM |
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 | ||
9706707d | 95 | view.setTitle( |
f6710aac | 96 | Ext.String.format('{0} {1} {2}', gettext('Migrate'), vm.get(view.vmtype).commonName, view.vmid), |
9706707d TM |
97 | ); |
98 | me.lookup('proxmoxHelpButton').setHelpConfig({ | |
f6710aac | 99 | onlineHelp: vm.get(view.vmtype).onlineHelp, |
9706707d | 100 | }); |
9706707d | 101 | me.lookup('formPanel').isValid(); |
9706707d TM |
102 | }, |
103 | ||
8058410f | 104 | onTargetChange: function(nodeSelector) { |
d8d4c14b | 105 | // Always display the storages of the currently seleceted migration target |
9706707d TM |
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 = { | |
f6710aac | 117 | target: values.target, |
9706707d TM |
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 | } | |
11c17a01 FE |
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) { | |
9706707d TM |
129 | params.targetstorage = values.targetstorage; |
130 | } | |
131 | ||
4ddaf290 | 132 | if (vm.get('migration.overwriteLocalResourceCheck')) { |
399ffa76 | 133 | params.force = 1; |
4ddaf290 TM |
134 | } |
135 | ||
9706707d TM |
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, | |
f6710aac | 150 | extraTitle: extraTitle, |
9706707d TM |
151 | }).show(); |
152 | ||
153 | view.close(); | |
f6710aac | 154 | }, |
9706707d | 155 | }); |
9706707d TM |
156 | }, |
157 | ||
6d727f81 | 158 | checkMigratePreconditions: async function(resetMigrationPossible) { |
9706707d TM |
159 | var me = this, |
160 | vm = me.getViewModel(); | |
161 | ||
9706707d TM |
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 | ||
6d727f81 DC |
168 | me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')]; |
169 | ||
9706707d | 170 | if (vm.get('vmtype') === 'qemu') { |
6d727f81 | 171 | await me.checkQemuPreconditions(resetMigrationPossible); |
9706707d | 172 | } else { |
4ddaf290 | 173 | me.checkLxcPreconditions(resetMigrationPossible); |
9706707d | 174 | } |
9706707d TM |
175 | |
176 | // Only allow nodes where the local storage is available in case of offline migration | |
177 | // where storage migration is not possible | |
178 | me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes'); | |
179 | ||
180 | me.lookup('formPanel').isValid(); | |
9706707d TM |
181 | }, |
182 | ||
d8d4c14b TL |
183 | checkQemuPreconditions: async function(resetMigrationPossible) { |
184 | let me = this, | |
9706707d TM |
185 | vm = me.getViewModel(), |
186 | migrateStats; | |
187 | ||
188 | if (vm.get('running')) { | |
189 | vm.set('migration.mode', 'online'); | |
190 | } | |
191 | ||
d8d4c14b | 192 | try { |
685b52f5 TL |
193 | if (me.fetchingNodeMigrateInfo && me.fetchingNodeMigrateInfo === vm.get('nodename')) { |
194 | return; | |
195 | } | |
196 | me.fetchingNodeMigrateInfo = vm.get('nodename'); | |
d8d4c14b TL |
197 | let { result } = await Proxmox.Async.api2({ |
198 | url: `/nodes/${vm.get('nodename')}/${vm.get('vmtype')}/${vm.get('vmid')}/migrate`, | |
199 | method: 'GET', | |
200 | }); | |
201 | migrateStats = result.data; | |
685b52f5 | 202 | me.fetchingNodeMigrateInfo = false; |
d8d4c14b TL |
203 | } catch (error) { |
204 | Ext.Msg.alert(gettext('Error'), error.htmlStatus); | |
205 | return; | |
206 | } | |
2df7ea9f | 207 | |
d8d4c14b TL |
208 | if (migrateStats.running) { |
209 | vm.set('running', true); | |
210 | } | |
211 | // Get migration object from viewmodel to prevent to many bind callbacks | |
212 | let migration = vm.get('migration'); | |
213 | if (resetMigrationPossible) { | |
214 | migration.possible = true; | |
215 | } | |
216 | migration.preconditions = []; | |
217 | ||
218 | if (migrateStats.allowed_nodes) { | |
219 | migration.allowedNodes = migrateStats.allowed_nodes; | |
220 | let target = me.lookup('pveNodeSelector').value; | |
221 | if (target.length && !migrateStats.allowed_nodes.includes(target)) { | |
6d727f81 | 222 | let disallowed = migrateStats.not_allowed_nodes[target] ?? {}; |
02f14aa8 DC |
223 | if (disallowed.unavailable_storages !== undefined) { |
224 | let missingStorages = disallowed.unavailable_storages.join(', '); | |
d8d4c14b | 225 | |
02f14aa8 DC |
226 | migration.possible = false; |
227 | migration.preconditions.push({ | |
228 | text: 'Storage (' + missingStorages + ') not available on selected target. ' + | |
229 | 'Start VM to use live storage migration or select other target node', | |
230 | severity: 'error', | |
231 | }); | |
232 | } | |
233 | ||
234 | if (disallowed['unavailable-resources'] !== undefined) { | |
235 | let unavailableResources = disallowed['unavailable-resources'].join(', '); | |
236 | ||
237 | migration.possible = false; | |
238 | migration.preconditions.push({ | |
239 | text: 'Mapped Resources (' + unavailableResources + ') not available on selected target. ', | |
240 | severity: 'error', | |
241 | }); | |
242 | } | |
d8d4c14b TL |
243 | } |
244 | } | |
245 | ||
02f14aa8 DC |
246 | let blockingResources = []; |
247 | let mappedResources = migrateStats['mapped-resources'] ?? []; | |
248 | ||
249 | for (const res of migrateStats.local_resources) { | |
250 | if (mappedResources.indexOf(res) === -1) { | |
251 | blockingResources.push(res); | |
252 | } | |
253 | } | |
254 | ||
255 | if (blockingResources.length) { | |
d8d4c14b TL |
256 | migration.hasLocalResources = true; |
257 | if (!migration.overwriteLocalResourceCheck || vm.get('running')) { | |
258 | migration.possible = false; | |
259 | migration.preconditions.push({ | |
260 | text: Ext.String.format('Can\'t migrate VM with local resources: {0}', | |
02f14aa8 | 261 | blockingResources.join(', ')), |
d8d4c14b TL |
262 | severity: 'error', |
263 | }); | |
264 | } else { | |
265 | migration.preconditions.push({ | |
266 | text: Ext.String.format('Migrate VM with local resources: {0}. ' + | |
267 | 'This might fail if resources aren\'t available on the target node.', | |
02f14aa8 | 268 | blockingResources.join(', ')), |
d8d4c14b TL |
269 | severity: 'warning', |
270 | }); | |
271 | } | |
272 | } | |
9706707d | 273 | |
02f14aa8 DC |
274 | if (mappedResources && mappedResources.length) { |
275 | if (vm.get('running')) { | |
276 | migration.possible = false; | |
277 | migration.preconditions.push({ | |
278 | text: Ext.String.format('Can\'t migrate running VM with mapped resources: {0}', | |
279 | mappedResources.join(', ')), | |
280 | severity: 'error', | |
281 | }); | |
282 | } | |
283 | } | |
284 | ||
d8d4c14b TL |
285 | if (migrateStats.local_disks.length) { |
286 | migrateStats.local_disks.forEach(function(disk) { | |
287 | if (disk.cdrom && disk.cdrom === 1) { | |
4d66fc7f | 288 | if (!disk.volid.includes('vm-' + vm.get('vmid') + '-cloudinit')) { |
4ddaf290 TM |
289 | migration.possible = false; |
290 | migration.preconditions.push({ | |
d8d4c14b | 291 | text: "Can't migrate VM with local CD/DVD", |
f6710aac | 292 | severity: 'error', |
4ddaf290 | 293 | }); |
4ddaf290 | 294 | } |
d8d4c14b TL |
295 | } else { |
296 | let size = disk.size ? '(' + Proxmox.Utils.render_size(disk.size) + ')' : ''; | |
297 | migration['with-local-disks'] = 1; | |
298 | migration.preconditions.push({ | |
299 | text: Ext.String.format('Migration with local disk might take long: {0} {1}', disk.volid, size), | |
300 | severity: 'warning', | |
9706707d | 301 | }); |
9706707d | 302 | } |
d8d4c14b TL |
303 | }); |
304 | } | |
9706707d | 305 | |
d8d4c14b | 306 | vm.set('migration', migration); |
9706707d | 307 | }, |
4ddaf290 | 308 | checkLxcPreconditions: function(resetMigrationPossible) { |
d8d4c14b | 309 | let vm = this.getViewModel(); |
9706707d TM |
310 | if (vm.get('running')) { |
311 | vm.set('migration.mode', 'restart'); | |
04c32bed | 312 | } |
f6710aac | 313 | }, |
2df7ea9f | 314 | }, |
04c32bed | 315 | |
3188b0b7 | 316 | width: 600, |
2df7ea9f | 317 | modal: true, |
9706707d TM |
318 | layout: { |
319 | type: 'vbox', | |
f6710aac | 320 | align: 'stretch', |
9706707d | 321 | }, |
2df7ea9f | 322 | border: false, |
2df7ea9f EK |
323 | items: [ |
324 | { | |
325 | xtype: 'form', | |
326 | reference: 'formPanel', | |
04c32bed DM |
327 | bodyPadding: 10, |
328 | border: false, | |
d27a44a6 | 329 | layout: 'hbox', |
04c32bed DM |
330 | items: [ |
331 | { | |
9706707d | 332 | xtype: 'container', |
d27a44a6 | 333 | flex: 1, |
9706707d | 334 | items: [{ |
090dff2f TL |
335 | xtype: 'displayfield', |
336 | name: 'source', | |
337 | fieldLabel: gettext('Source node'), | |
338 | bind: { | |
f6710aac TL |
339 | value: '{nodename}', |
340 | }, | |
9706707d TM |
341 | }, |
342 | { | |
343 | xtype: 'displayfield', | |
344 | reference: 'migrationMode', | |
345 | fieldLabel: gettext('Mode'), | |
346 | bind: { | |
f6710aac TL |
347 | value: '{setMigrationMode}', |
348 | }, | |
349 | }], | |
04c32bed DM |
350 | }, |
351 | { | |
9706707d | 352 | xtype: 'container', |
d27a44a6 | 353 | flex: 1, |
090dff2f TL |
354 | items: [{ |
355 | xtype: 'pveNodeSelector', | |
356 | reference: 'pveNodeSelector', | |
357 | name: 'target', | |
358 | fieldLabel: gettext('Target node'), | |
359 | allowBlank: false, | |
360 | disallowedNodes: undefined, | |
361 | onlineValidator: true, | |
362 | listeners: { | |
f6710aac TL |
363 | change: 'onTargetChange', |
364 | }, | |
090dff2f TL |
365 | }, |
366 | { | |
367 | xtype: 'pveStorageSelector', | |
368 | reference: 'pveDiskStorageSelector', | |
369 | name: 'targetstorage', | |
370 | fieldLabel: gettext('Target storage'), | |
371 | storageContent: 'images', | |
11c17a01 FE |
372 | allowBlank: true, |
373 | autoSelect: false, | |
cd338fe1 | 374 | emptyText: gettext('Current layout'), |
090dff2f | 375 | bind: { |
f6710aac TL |
376 | hidden: '{setStorageselectorHidden}', |
377 | }, | |
4ddaf290 TM |
378 | }, |
379 | { | |
380 | xtype: 'proxmoxcheckbox', | |
381 | name: 'overwriteLocalResourceCheck', | |
382 | fieldLabel: gettext('Force'), | |
383 | autoEl: { | |
384 | tag: 'div', | |
f6710aac | 385 | 'data-qtip': 'Overwrite local resources unavailable check', |
4ddaf290 TM |
386 | }, |
387 | bind: { | |
388 | hidden: '{setLocalResourceCheckboxHidden}', | |
f6710aac | 389 | value: '{migration.overwriteLocalResourceCheck}', |
4ddaf290 TM |
390 | }, |
391 | listeners: { | |
d8d4c14b TL |
392 | change: { |
393 | fn: 'checkMigratePreconditions', | |
394 | extraArg: true, | |
395 | }, | |
f6710aac TL |
396 | }, |
397 | }], | |
398 | }, | |
399 | ], | |
9706707d TM |
400 | }, |
401 | { | |
402 | xtype: 'gridpanel', | |
403 | reference: 'preconditionGrid', | |
b4533390 | 404 | selectable: false, |
9706707d | 405 | flex: 1, |
b4533390 TL |
406 | columns: [{ |
407 | text: '', | |
408 | dataIndex: 'severity', | |
409 | renderer: function(v) { | |
410 | switch (v) { | |
411 | case 'warning': | |
412 | return '<i class="fa fa-exclamation-triangle warning"></i> '; | |
413 | case 'error': | |
414 | return '<i class="fa fa-times critical"></i>'; | |
415 | default: | |
416 | return v; | |
417 | } | |
418 | }, | |
f6710aac | 419 | width: 35, |
b4533390 TL |
420 | }, |
421 | { | |
422 | text: 'Info', | |
423 | dataIndex: 'text', | |
424 | cellWrap: true, | |
f6710aac | 425 | flex: 1, |
b4533390 | 426 | }], |
9706707d TM |
427 | bind: { |
428 | hidden: '{!migration.preconditions.length}', | |
429 | store: { | |
cd338fe1 TL |
430 | fields: ['severity', 'text'], |
431 | data: '{migration.preconditions}', | |
432 | sorters: 'text', | |
f6710aac TL |
433 | }, |
434 | }, | |
435 | }, | |
9706707d | 436 | |
2df7ea9f EK |
437 | ], |
438 | buttons: [ | |
439 | { | |
672a6270 TL |
440 | xtype: 'proxmoxHelpButton', |
441 | reference: 'proxmoxHelpButton', | |
2df7ea9f EK |
442 | onlineHelp: 'pct_migration', |
443 | listenToGlobalEvent: false, | |
f6710aac | 444 | hidden: false, |
2df7ea9f EK |
445 | }, |
446 | '->', | |
447 | { | |
448 | xtype: 'button', | |
449 | reference: 'submitButton', | |
9706707d TM |
450 | text: gettext('Migrate'), |
451 | handler: 'startMigration', | |
452 | bind: { | |
f6710aac TL |
453 | disabled: '{!migration.possible}', |
454 | }, | |
455 | }, | |
456 | ], | |
3188b0b7 | 457 | }); |