]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/window/GuestImport.js
ui: guest import: add value-dependent warning for live-import
[pve-manager.git] / www / manager6 / window / GuestImport.js
1 Ext.define('PVE.window.GuestImport', {
2 extend: 'Proxmox.window.Edit', // fixme: Proxmox.window.Edit?
3 alias: 'widget.pveGuestImportWindow',
4
5 title: gettext('Import Guest'),
6
7 submitUrl: function() {
8 let me = this;
9 return `/nodes/${me.nodename}/qemu`;
10 },
11
12 isAdd: true,
13 isCreate: true,
14 submitText: gettext('Import'),
15 showTaskViewer: true,
16 method: 'POST',
17
18 loadUrl: function(_url, { storage, nodename, volumeName }) {
19 let args = Ext.Object.toQueryString({ volume: volumeName });
20 return `/nodes/${nodename}/storage/${storage}/import-metadata?${args}`;
21 },
22
23 controller: {
24 xclass: 'Ext.app.ViewController',
25
26 setNodename: function(_column, widget) {
27 let me = this;
28 let view = me.getView();
29 widget.setNodename(view.nodename);
30 },
31
32 diskStorageChange: function(storageSelector, value) {
33 let me = this;
34
35 let grid = me.lookup('diskGrid');
36 let rec = storageSelector.getWidgetRecord();
37 let validFormats = storageSelector.store.getById(value)?.data.format;
38 grid.query('pveDiskFormatSelector').some((selector) => {
39 if (selector.getWidgetRecord().data.id !== rec.data.id) {
40 return false;
41 }
42
43 if (validFormats?.[0]?.qcow2) {
44 selector.setDisabled(false);
45 selector.setValue('qcow2');
46 } else {
47 selector.setValue('raw');
48 selector.setDisabled(true);
49 }
50
51 return true;
52 });
53 },
54
55 isoStorageChange: function(storageSelector, value) {
56 let me = this;
57
58 let grid = me.lookup('cdGrid');
59 let rec = storageSelector.getWidgetRecord();
60 grid.query('pveFileSelector').some((selector) => {
61 if (selector.getWidgetRecord().data.id !== rec.data.id) {
62 return false;
63 }
64
65 selector.setStorage(value);
66 if (!value) {
67 selector.setValue('');
68 }
69
70 return true;
71 });
72 },
73
74 onOSBaseChange: function(_field, value) {
75 let me = this;
76 let ostype = me.lookup('ostype');
77 let store = ostype.getStore();
78 store.setData(PVE.Utils.kvm_ostypes[value]);
79 let old_val = ostype.getValue();
80 if (old_val && store.find('val', old_val) !== -1) {
81 ostype.setValue(old_val);
82 } else {
83 ostype.setValue(store.getAt(0));
84 }
85 },
86
87 calculateConfig: function() {
88 let me = this;
89 let inputPanel = me.lookup('mainInputPanel');
90 let summaryGrid = me.lookup('summaryGrid');
91 let values = inputPanel.getValues();
92 summaryGrid.getStore().setData(Object.entries(values).map(([key, value]) => ({ key, value })));
93 },
94
95 control: {
96 'grid field': {
97 // update records from widgetcolumns
98 change: function(widget, value) {
99 let rec = widget.getWidgetRecord();
100 rec.set(widget.name, value);
101 rec.commit();
102 },
103 },
104 'grid[reference=diskGrid] pveStorageSelector': {
105 change: 'diskStorageChange',
106 },
107 'grid[reference=cdGrid] pveStorageSelector': {
108 change: 'isoStorageChange',
109 },
110 'field[name=osbase]': {
111 change: 'onOSBaseChange',
112 },
113 'panel[reference=summaryTab]': {
114 activate: 'calculateConfig',
115 },
116 },
117 },
118
119 viewModel: {
120 data: {
121 coreCount: 1,
122 socketCount: 1,
123 liveImport: false,
124 warnings: [],
125 },
126
127 formulas: {
128 totalCoreCount: get => get('socketCount') * get('coreCount'),
129 hideWarnings: get => get('warnings').length === 0,
130 warningsText: get => '<ul style="margin: 0; padding-left: 20px;">'
131 + get('warnings').map(w => `<li>${w}</li>`).join('') + '</ul>',
132 liveImportNote: get => !get('liveImport') ? ''
133 : gettext('Note: If anything goes wrong during the live-import, new data written by the VM may be lost.'),
134 },
135 },
136
137 width: 700,
138 bodyPadding: 0,
139
140 items: [
141 {
142 xtype: 'tabpanel',
143 defaults: {
144 bodyPadding: 10,
145 },
146 items: [
147 {
148 title: gettext('General'),
149 xtype: 'inputpanel',
150 reference: 'mainInputPanel',
151 onGetValues: function(values) {
152 let me = this;
153 let grid = me.up('pveGuestImportWindow');
154
155 // from pveDiskStorageSelector
156 let defaultStorage = values.hdstorage;
157 let defaultFormat = values.diskformat;
158 delete values.hdstorage;
159 delete values.diskformat;
160
161 let defaultBridge = values.defaultBridge;
162 delete values.defaultBridge;
163
164 let config = Ext.apply(grid.vmConfig, values);
165
166 if (config.scsi0) {
167 config.scsi0 = config.scsi0.replace('local:0,', 'local:0,format=qcow2,');
168 }
169
170 grid.lookup('diskGrid').getStore().each((rec) => {
171 if (!rec.data.enable) {
172 return;
173 }
174 let id = rec.data.id;
175 let data = {
176 ...rec.data,
177 };
178 delete data.enable;
179 delete data.id;
180 delete data.size;
181 if (!data.file) {
182 data.file = defaultStorage;
183 data.format = defaultFormat;
184 }
185 data.file += ':0'; // for our special api format
186 if (id === 'efidisk0') {
187 delete data['import-from'];
188 }
189 config[id] = PVE.Parser.printQemuDrive(data);
190 });
191
192 grid.lookup('netGrid').getStore().each((rec) => {
193 if (!rec.data.enable) {
194 return;
195 }
196 let id = rec.data.id;
197 let data = {
198 ...rec.data,
199 };
200 delete data.enable;
201 delete data.id;
202 if (!data.bridge) {
203 data.bridge = defaultBridge;
204 }
205 config[id] = PVE.Parser.printQemuNetwork(data);
206 });
207
208 grid.lookup('cdGrid').getStore().each((rec) => {
209 if (!rec.data.enable) {
210 return;
211 }
212 let id = rec.data.id;
213 let cd = {
214 media: 'cdrom',
215 file: rec.data.file ? rec.data.file : 'none',
216 };
217 config[id] = PVE.Parser.printPropertyString(cd);
218 });
219
220 if (grid.lookup('liveimport').getValue()) {
221 config['live-restore'] = 1;
222 }
223
224 return config;
225 },
226
227 column1: [
228 {
229 xtype: 'pveGuestIDSelector',
230 name: 'vmid',
231 fieldLabel: 'VM',
232 guestType: 'qemu',
233 loadNextFreeID: true,
234 },
235 {
236 xtype: 'proxmoxintegerfield',
237 fieldLabel: gettext('Sockets'),
238 name: 'sockets',
239 reference: 'socketsField',
240 value: 1,
241 minValue: 1,
242 maxValue: 4,
243 allowBlank: true,
244 bind: {
245 value: '{socketCount}',
246 },
247 },
248 {
249 xtype: 'proxmoxintegerfield',
250 fieldLabel: gettext('Cores'),
251 name: 'cores',
252 reference: 'coresField',
253 value: 1,
254 minValue: 1,
255 maxValue: 128,
256 allowBlank: true,
257 bind: {
258 value: '{coreCount}',
259 },
260 },
261 {
262 xtype: 'pveMemoryField',
263 fieldLabel: gettext('Memory'),
264 name: 'memory',
265 reference: 'memoryField',
266 value: 512,
267 allowBlank: true,
268 },
269 {
270 //spacer
271 xtype: 'displayfield',
272 },
273 {
274 xtype: 'pveDiskStorageSelector',
275 reference: 'defaultStorage',
276 storageLabel: gettext('Default Storage'),
277 storageContent: 'images',
278 autoSelect: true,
279 hideSize: true,
280 name: 'defaultStorage',
281 },
282 ],
283
284 column2: [
285 {
286 xtype: 'textfield',
287 fieldLabel: gettext('Name'),
288 name: 'name',
289 vtype: 'DnsName',
290 reference: 'nameField',
291 allowBlank: true,
292 },
293 {
294 xtype: 'CPUModelSelector',
295 name: 'cpu',
296 reference: 'cputype',
297 value: 'x86-64-v2-AES',
298 fieldLabel: gettext('Type'),
299 },
300 {
301 xtype: 'displayfield',
302 fieldLabel: gettext('Total cores'),
303 name: 'totalcores',
304 isFormField: false,
305 bind: {
306 value: '{totalCoreCount}',
307 },
308 },
309 {
310 xtype: 'combobox',
311 submitValue: false,
312 name: 'osbase',
313 fieldLabel: gettext('OS Type'),
314 editable: false,
315 queryMode: 'local',
316 value: 'Linux',
317 store: Object.keys(PVE.Utils.kvm_ostypes),
318 },
319 {
320 xtype: 'combobox',
321 name: 'ostype',
322 reference: 'ostype',
323 fieldLabel: gettext('Version'),
324 value: 'l26',
325 allowBlank: false,
326 editable: false,
327 queryMode: 'local',
328 valueField: 'val',
329 displayField: 'desc',
330 store: {
331 fields: ['desc', 'val'],
332 data: PVE.Utils.kvm_ostypes.Linux,
333 },
334 },
335 {
336 xtype: 'PVE.form.BridgeSelector',
337 reference: 'defaultBridge',
338 name: 'defaultBridge',
339 allowBlank: false,
340 fieldLabel: gettext('Default Bridge'),
341 },
342 ],
343
344 columnB: [
345 {
346 xtype: 'proxmoxcheckbox',
347 fieldLabel: gettext('Live Import'),
348 reference: 'liveimport',
349 isFormField: false,
350 boxLabelCls: 'pmx-hint black x-form-cb-label',
351 bind: {
352 value: '{liveImport}',
353 boxLabel: '{liveImportNote}',
354 },
355 },
356 {
357 xtype: 'displayfield',
358 fieldLabel: gettext('Warnings'),
359 labelWidth: 200,
360 hidden: true,
361 bind: {
362 hidden: '{hideWarnings}',
363 },
364 },
365 {
366 xtype: 'displayfield',
367 reference: 'warningText',
368 userCls: 'pmx-hint',
369 hidden: true,
370 bind: {
371 hidden: '{hideWarnings}',
372 value: '{warningsText}',
373 },
374 },
375 ],
376 },
377 {
378 title: gettext('Advanced'),
379 xtype: 'inputpanel',
380 items: [
381 {
382 xtype: 'displayfield',
383 fieldLabel: gettext('Disks'),
384 labelWidth: 200,
385 },
386 {
387 xtype: 'grid',
388 reference: 'diskGrid',
389 minHeight: 58,
390 maxHeight: 150,
391 store: {
392 data: [],
393 sorters: [
394 'id',
395 ],
396 },
397 columns: [
398 {
399 xtype: 'checkcolumn',
400 header: gettext('Use'),
401 width: 50,
402 dataIndex: 'enable',
403 listeners: {
404 checkchange: function(_column, _rowIndex, _checked, record) {
405 record.commit();
406 },
407 },
408 },
409 {
410 text: gettext('Disk'),
411 dataIndex: 'id',
412 },
413 {
414 text: gettext('Source'),
415 dataIndex: 'import-from',
416 flex: 1,
417 renderer: function(value) {
418 return value.replace(/^.*\//, '');
419 },
420 },
421 {
422 text: gettext('Size'),
423 dataIndex: 'size',
424 renderer: (value) => {
425 if (Ext.isNumeric(value)) {
426 return Proxmox.Utils.render_size(value);
427 }
428 return value ?? Proxmox.Utils.unknownText;
429 },
430 },
431 {
432 text: gettext('Storage'),
433 dataIndex: 'file',
434 xtype: 'widgetcolumn',
435 width: 150,
436 widget: {
437 xtype: 'pveStorageSelector',
438 isFormField: false,
439 autoSelect: false,
440 allowBlank: true,
441 emptyText: gettext('From Default'),
442 name: 'file',
443 storageContent: 'images',
444 },
445 onWidgetAttach: 'setNodename',
446 },
447 {
448 text: gettext('Format'),
449 dataIndex: 'format',
450 xtype: 'widgetcolumn',
451 width: 150,
452 widget: {
453 xtype: 'pveDiskFormatSelector',
454 name: 'format',
455 disabled: true,
456 isFormField: false,
457 matchFieldWidth: false,
458 },
459 },
460 ],
461 },
462 {
463 xtype: 'displayfield',
464 fieldLabel: gettext('CD/DVD Drives'),
465 labelWidth: 200,
466 },
467 {
468 xtype: 'grid',
469 reference: 'cdGrid',
470 minHeight: 58,
471 maxHeight: 150,
472 store: {
473 data: [],
474 sorters: [
475 'id',
476 ],
477 },
478 columns: [
479 {
480 xtype: 'checkcolumn',
481 header: gettext('Use'),
482 width: 50,
483 dataIndex: 'enable',
484 listeners: {
485 checkchange: function(_column, _rowIndex, _checked, record) {
486 record.commit();
487 },
488 },
489 },
490 {
491 text: gettext('Slot'),
492 dataIndex: 'id',
493 sorted: true,
494 },
495 {
496 text: gettext('Storage'),
497 xtype: 'widgetcolumn',
498 width: 150,
499 widget: {
500 xtype: 'pveStorageSelector',
501 isFormField: false,
502 autoSelect: false,
503 allowBlank: true,
504 emptyText: Proxmox.Utils.noneText,
505 storageContent: 'iso',
506 },
507 onWidgetAttach: 'setNodename',
508 },
509 {
510 text: gettext('ISO'),
511 dataIndex: 'file',
512 xtype: 'widgetcolumn',
513 flex: 1,
514 widget: {
515 xtype: 'pveFileSelector',
516 name: 'file',
517 isFormField: false,
518 allowBlank: true,
519 emptyText: Proxmox.Utils.noneText,
520 storageContent: 'iso',
521 },
522 onWidgetAttach: 'setNodename',
523 },
524 ],
525 },
526 {
527 xtype: 'displayfield',
528 fieldLabel: gettext('Network Interfaces'),
529 labelWidth: 200,
530 },
531 {
532 xtype: 'grid',
533 minHeight: 58,
534 maxHeight: 150,
535 reference: 'netGrid',
536 store: {
537 data: [],
538 sorters: [
539 'id',
540 ],
541 },
542 columns: [
543 {
544 xtype: 'checkcolumn',
545 header: gettext('Use'),
546 width: 50,
547 dataIndex: 'enable',
548 listeners: {
549 checkchange: function(_column, _rowIndex, _checked, record) {
550 record.commit();
551 },
552 },
553 },
554 {
555 text: gettext('ID'),
556 dataIndex: 'id',
557 },
558 {
559 text: gettext('MAC address'),
560 flex: 1,
561 dataIndex: 'macaddr',
562 renderer: value => value ?? 'auto',
563 },
564 {
565 text: gettext('Model'),
566 flex: 1,
567 dataIndex: 'model',
568 xtype: 'widgetcolumn',
569 widget: {
570 xtype: 'pveNetworkCardSelector',
571 name: 'model',
572 isFormField: false,
573 allowBlank: false,
574 },
575 },
576 {
577 text: gettext('Bridge'),
578 dataIndex: 'bridge',
579 xtype: 'widgetcolumn',
580 flex: 1,
581 widget: {
582 xtype: 'PVE.form.BridgeSelector',
583 name: 'bridge',
584 isFormField: false,
585 autoSelect: false,
586 allowBlank: true,
587 emptyText: gettext('From Default'),
588 },
589 onWidgetAttach: 'setNodename',
590 },
591 ],
592 },
593 ],
594 },
595 {
596 title: gettext('Resulting Config'),
597 reference: 'summaryTab',
598 items: [
599 {
600 xtype: 'grid',
601 reference: 'summaryGrid',
602 maxHeight: 400,
603 scrollable: true,
604 store: {
605 model: 'KeyValue',
606 sorters: [{
607 property: 'key',
608 direction: 'ASC',
609 }],
610 },
611 columns: [
612 { header: 'Key', width: 150, dataIndex: 'key' },
613 { header: 'Value', flex: 1, dataIndex: 'value' },
614 ],
615 },
616 ],
617 },
618 ],
619 },
620 ],
621
622 initComponent: function() {
623 let me = this;
624
625 if (!me.volumeName) {
626 throw "no volumeName given";
627 }
628
629 if (!me.storage) {
630 throw "no storage given";
631 }
632
633 if (!me.nodename) {
634 throw "no nodename given";
635 }
636
637 me.callParent();
638
639 me.setTitle(Ext.String.format(gettext('Import Guest - {0}'), `${me.storage}:${me.volumeName}`));
640
641 me.lookup('defaultStorage').setNodename(me.nodename);
642 me.lookup('defaultBridge').setNodename(me.nodename);
643
644 let renderWarning = w => {
645 const warningsCatalogue = {
646 'cdrom-image-ignored': gettext("CD-ROM images cannot get imported, if required you can reconfigure the '{0}' drive in the 'Advanced' tab."),
647 'nvme-unsupported': gettext("NVMe disks are currently not supported, '{0}' will get attaced as SCSI"),
648 'ovmf-with-lsi-unsupported': gettext("OVMF is built without LSI drivers, scsi hardware was set to '{1}'"),
649 'serial-port-socket-only': gettext("Serial socket '{0}' will be mapped to a socket"),
650 'guest-is-running': gettext('Virtual guest seems to be running on source host. Import might fail or have inconsistent state!'),
651 };
652 let message = warningsCatalogue[w.type];
653 if (!w.type || !message) {
654 return w.message ?? w.type ?? gettext('Unknown warning');
655 }
656 return Ext.String.format(message, w.key ?? 'unknown', w.value ?? 'unknown');
657 };
658
659 me.load({
660 success: function(response) {
661 let data = response.result.data;
662 me.vmConfig = data['create-args'];
663
664 let disks = [];
665 for (const [id, value] of Object.entries(data.disks ?? {})) {
666 let volid = Ext.htmlEncode('<none>');
667 let size = 'auto';
668 if (Ext.isObject(value)) {
669 volid = value.volid;
670 size = value.size;
671 }
672 disks.push({
673 id,
674 enable: true,
675 size,
676 'import-from': volid,
677 format: 'raw',
678 });
679 }
680
681 let nets = [];
682 for (const [id, parsed] of Object.entries(data.net ?? {})) {
683 parsed.id = id;
684 parsed.enable = true;
685 nets.push(parsed);
686 }
687
688 let cdroms = [];
689 for (const [id, value] of Object.entries(me.vmConfig)) {
690 if (!Ext.isString(value) || !value.match(/media=cdrom/)) {
691 continue;
692 }
693 cdroms.push({
694 enable: true,
695 id,
696 });
697 delete me.vmConfig[id];
698 }
699 me.lookup('diskGrid').getStore().setData(disks);
700 me.lookup('netGrid').getStore().setData(nets);
701 me.lookup('cdGrid').getStore().setData(cdroms);
702
703 me.getViewModel().set('warnings', data.warnings.map(w => renderWarning(w)));
704
705 let osinfo = PVE.Utils.get_kvm_osinfo(me.vmConfig.ostype ?? '');
706
707 me.setValues({
708 osbase: osinfo.base,
709 ...me.vmConfig,
710 });
711 },
712 });
713 },
714 });