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