]>
git.proxmox.com Git - pve-manager.git/blob - www/manager6/window/GuestImport.js
1 Ext
.define('PVE.window.GuestImport', {
2 extend
: 'Proxmox.window.Edit', // fixme: Proxmox.window.Edit?
3 alias
: 'widget.pveGuestImportWindow',
5 title
: gettext('Import Guest'),
10 submitUrl: function() {
12 return `/nodes/${me.nodename}/qemu`;
17 submitText
: gettext('Import'),
21 loadUrl: function(_url
, { storage
, nodename
, volumeName
}) {
22 let args
= Ext
.Object
.toQueryString({ volume
: volumeName
});
23 return `/nodes/${nodename}/storage/${storage}/import-metadata?${args}`;
27 xclass
: 'Ext.app.ViewController',
29 setNodename: function(_column
, widget
) {
31 let view
= me
.getView();
32 widget
.setNodename(view
.nodename
);
35 diskStorageChange: function(storageSelector
, value
) {
38 let grid
= me
.lookup('diskGrid');
39 let rec
= storageSelector
.getWidgetRecord();
40 let validFormats
= storageSelector
.store
.getById(value
)?.data
.format
;
41 grid
.query('pveDiskFormatSelector').some((selector
) => {
42 if (selector
.getWidgetRecord().data
.id
!== rec
.data
.id
) {
46 if (validFormats
?.[0]?.qcow2
) {
47 selector
.setDisabled(false);
48 selector
.setValue('qcow2');
50 selector
.setValue('raw');
51 selector
.setDisabled(true);
58 isoStorageChange: function(storageSelector
, value
) {
61 let grid
= me
.lookup('cdGrid');
62 let rec
= storageSelector
.getWidgetRecord();
63 grid
.query('pveFileSelector').some((selector
) => {
64 if (selector
.getWidgetRecord().data
.id
!== rec
.data
.id
) {
68 selector
.setStorage(value
);
70 selector
.setValue('');
77 onOSBaseChange: function(_field
, value
) {
79 let ostype
= me
.lookup('ostype');
80 let store
= ostype
.getStore();
81 store
.setData(PVE
.Utils
.kvm_ostypes
[value
]);
82 let old_val
= ostype
.getValue();
83 if (old_val
&& store
.find('val', old_val
) !== -1) {
84 ostype
.setValue(old_val
);
86 ostype
.setValue(store
.getAt(0));
90 calculateConfig: function() {
92 let inputPanel
= me
.lookup('mainInputPanel');
93 let summaryGrid
= me
.lookup('summaryGrid');
94 let values
= inputPanel
.getValues();
95 summaryGrid
.getStore().setData(Object
.entries(values
).map(([key
, value
]) => ({ key
, value
})));
98 calculateAdditionalCDIdx: function() {
101 let maxIde
= me
.getMaxControllerId('ide');
102 let maxSata
= me
.getMaxControllerId('sata');
103 // only ide0 and ide2 can be used reliably for isos (e.g. for q35)
110 if (maxSata
< PVE
.Utils
.diskControllerMaxIDs
.sata
- 1) {
111 return `sata${maxSata+1}`;
117 // assume assigned sata disks indices are continuous, so without holes
118 getMaxControllerId: function(controller
) {
120 let view
= me
.getView();
125 let max
= view
[`max${controller}`];
126 if (max
!== undefined) {
131 for (const key
of Object
.keys(me
.getView().vmConfig
)) {
132 if (!key
.toLowerCase().startsWith(controller
)) {
135 let idx
= parseInt(key
.slice(controller
.length
), 10);
140 me
.lookup('diskGrid').getStore().each(rec
=> {
141 if (!rec
.data
.id
.toLowerCase().startsWith(controller
)) {
144 let idx
= parseInt(rec
.data
.id
.slice(controller
.length
), 10);
149 me
.lookup('cdGrid').getStore().each(rec
=> {
150 if (!rec
.data
.id
.toLowerCase().startsWith(controller
) || rec
.data
.hidden
) {
153 let idx
= parseInt(rec
.data
.id
.slice(controller
.length
), 10);
159 view
[`max${controller}`] = max
;
163 renderDisk: function(value
, metaData
, record
, rowIndex
, colIndex
, store
, tableView
) {
164 let diskGrid
= tableView
.grid
?? this.lookup('diskGrid');
165 if (diskGrid
.diskMap
) {
166 let mappedID
= diskGrid
.diskMap
[value
];
169 if (mappedID
=== value
) { // mapped to the same value means we ran out of IDs
170 let warning
= gettext('Too many disks, could not map to SATA.');
171 prefix
= `<i data-qtip="${warning}" class="fa fa-exclamation-triangle warning"></i> `;
173 return `${prefix}${mappedID}`;
179 refreshGrids: function() {
180 this.lookup('diskGrid').reconfigure();
181 this.lookup('cdGrid').reconfigure();
182 this.lookup('netGrid').reconfigure();
185 onOSTypeChange: function(_cb
, value
) {
190 let store
= me
.lookup('cdGrid').getStore();
191 let collection
= store
.getData().getSource() ?? store
.getData();
192 let rec
= collection
.find('autogenerated', true);
194 let isWindows
= (value
?? '').startsWith('w');
196 rec
.set('hidden', !isWindows
);
199 let prepareVirtio
= me
.lookup('prepareForVirtIO').getValue();
200 let defaultScsiHw
= me
.getView().vmConfig
.scsihw
?? '__default__';
201 me
.lookup('scsihw').setValue(prepareVirtio
&& isWindows
? 'virtio-scsi-single' : defaultScsiHw
);
206 onPrepareVirtioChange: function(_cb
, value
) {
208 let view
= me
.getView();
209 let diskGrid
= me
.lookup('diskGrid');
211 diskGrid
.diskMap
= {};
213 const hasAdditionalSataCDROM
=
214 me
.getViewModel().get('isWindows') && view
.additionalCdIdx
?.startsWith('sata');
216 diskGrid
.getStore().each(rec
=> {
217 let diskID
= rec
.data
.id
;
218 if (!diskID
.toLowerCase().startsWith('scsi')) {
221 let offset
= parseInt(diskID
.slice(4), 10);
222 let newIdx
= offset
+ me
.getMaxControllerId('sata') + 1;
223 if (hasAdditionalSataCDROM
) {
226 let mappedID
= `sata${newIdx}`;
227 if (newIdx
>= PVE
.Utils
.diskControllerMaxIDs
.sata
) {
228 mappedID
= diskID
; // map to self so that the renderer can detect that we're out of IDs
230 diskGrid
.diskMap
[diskID
] = mappedID
;
234 let scsihw
= me
.lookup('scsihw');
235 scsihw
.suspendEvents();
236 scsihw
.setValue(value
? 'virtio-scsi-single' : me
.getView().vmConfig
.scsihw
);
237 scsihw
.resumeEvents();
242 onScsiHwChange: function(_field
, value
) {
244 me
.getView().vmConfig
.scsihw
= value
;
247 onUniqueMACChange: function(_cb
, value
) {
250 me
.getViewModel().set('uniqueMACAdresses', value
);
252 me
.lookup('netGrid').reconfigure();
255 renderMacAddress: function(value
, metaData
, record
, rowIndex
, colIndex
, store
, view
) {
257 let vm
= me
.getViewModel();
259 return !vm
.get('uniqueMACAdresses') && value
? value
: 'auto';
264 // update records from widgetcolumns
265 change: function(widget
, value
) {
266 let rec
= widget
.getWidgetRecord();
267 rec
.set(widget
.name
, value
);
271 'grid[reference=diskGrid] pveStorageSelector': {
272 change
: 'diskStorageChange',
274 'grid[reference=cdGrid] pveStorageSelector': {
275 change
: 'isoStorageChange',
277 'field[name=osbase]': {
278 change
: 'onOSBaseChange',
280 'panel[reference=summaryTab]': {
281 activate
: 'calculateConfig',
283 'proxmoxcheckbox[reference=prepareForVirtIO]': {
284 change
: 'onPrepareVirtioChange',
286 'combobox[name=ostype]': {
287 change
: 'onOSTypeChange',
289 'pveScsiHwSelector': {
290 change
: 'onScsiHwChange',
292 'proxmoxcheckbox[name=uniqueMACs]': {
293 change
: 'onUniqueMACChange',
305 uniqueMACAdresses
: false,
310 totalCoreCount
: get => get('socketCount') * get('coreCount'),
311 hideWarnings
: get => get('warnings').length
=== 0,
312 warningsText
: get => '<ul style="margin: 0; padding-left: 20px;">'
313 + get('warnings').map(w
=> `<li>${w}</li>`).join('') + '</ul>',
314 liveImportNote
: get => !get('liveImport') ? ''
315 : gettext('Note: If anything goes wrong during the live-import, new data written by the VM may be lost.'),
316 isWindows
: get => (get('os') ?? '').startsWith('w'),
327 title
: gettext('General'),
329 reference
: 'mainInputPanel',
330 onGetValues: function(values
) {
332 let view
= me
.up('pveGuestImportWindow');
333 let vm
= view
.getViewModel();
334 let diskGrid
= view
.lookup('diskGrid');
336 // from pveDiskStorageSelector
337 let defaultStorage
= values
.hdstorage
;
338 let defaultFormat
= values
.diskformat
;
339 delete values
.hdstorage
;
340 delete values
.diskformat
;
342 let defaultBridge
= values
.defaultBridge
;
343 delete values
.defaultBridge
;
345 let config
= { ...view
.vmConfig
};
346 Ext
.apply(config
, values
);
349 config
.scsi0
= config
.scsi0
.replace('local:0,', 'local:0,format=qcow2,');
352 let parsedBoot
= PVE
.Parser
.parsePropertyString(config
.boot
?? '');
353 if (parsedBoot
.order
) {
354 parsedBoot
.order
= parsedBoot
.order
.split(';');
357 let diskMap
= diskGrid
.diskMap
?? {};
358 diskGrid
.getStore().each(rec
=> {
359 if (!rec
.data
.enable
) {
362 let id
= diskMap
[rec
.data
.id
] ?? rec
.data
.id
;
363 if (id
!== rec
.data
.id
&& parsedBoot
?.order
) {
364 let idx
= parsedBoot
.order
.indexOf(rec
.data
.id
);
366 parsedBoot
.order
[idx
] = id
;
376 data
.file
= defaultStorage
;
377 data
.format
= defaultFormat
;
379 data
.file
+= ':0'; // for our special api format
380 if (id
=== 'efidisk0') {
381 delete data
['import-from'];
383 config
[id
] = PVE
.Parser
.printQemuDrive(data
);
386 if (parsedBoot
.order
) {
387 parsedBoot
.order
= parsedBoot
.order
.join(';');
389 config
.boot
= PVE
.Parser
.printPropertyString(parsedBoot
);
391 view
.lookup('netGrid').getStore().each((rec
) => {
392 if (!rec
.data
.enable
) {
395 let id
= rec
.data
.id
;
402 data
.bridge
= defaultBridge
;
404 if (vm
.get('uniqueMACAdresses')) {
405 data
.macaddr
= undefined;
407 config
[id
] = PVE
.Parser
.printQemuNetwork(data
);
410 view
.lookup('cdGrid').getStore().each((rec
) => {
411 if (!rec
.data
.enable
) {
414 let id
= rec
.data
.id
;
417 file
: rec
.data
.file
? rec
.data
.file
: 'none',
419 config
[id
] = PVE
.Parser
.printPropertyString(cd
);
422 config
.scsihw
= view
.lookup('scsihw').getValue();
424 if (view
.lookup('liveimport').getValue()) {
425 config
['live-restore'] = 1;
428 // remove __default__ values
429 for (const [key
, value
] of Object
.entries(config
)) {
430 if (value
=== '__default__') {
440 xtype
: 'pveGuestIDSelector',
444 loadNextFreeID
: true,
445 validateExists
: false,
448 xtype
: 'proxmoxintegerfield',
449 fieldLabel
: gettext('Sockets'),
451 reference
: 'socketsField',
457 value
: '{socketCount}',
461 xtype
: 'proxmoxintegerfield',
462 fieldLabel
: gettext('Cores'),
464 reference
: 'coresField',
470 value
: '{coreCount}',
474 xtype
: 'pveMemoryField',
475 fieldLabel
: gettext('Memory') + ' (MiB)',
477 reference
: 'memoryField',
481 { xtype
: 'displayfield' }, // spacer
482 { xtype
: 'displayfield' }, // spacer
484 xtype
: 'pveDiskStorageSelector',
485 reference
: 'defaultStorage',
486 storageLabel
: gettext('Default Storage'),
487 storageContent
: 'images',
490 name
: 'defaultStorage',
497 fieldLabel
: gettext('Name'),
500 reference
: 'nameField',
504 xtype
: 'CPUModelSelector',
506 reference
: 'cputype',
507 value
: 'x86-64-v2-AES',
508 fieldLabel
: gettext('CPU Type'),
511 xtype
: 'displayfield',
512 fieldLabel
: gettext('Total cores'),
516 value
: '{totalCoreCount}',
523 fieldLabel
: gettext('OS Type'),
527 store
: Object
.keys(PVE
.Utils
.kvm_ostypes
),
533 fieldLabel
: gettext('Version'),
539 displayField
: 'desc',
544 fields
: ['desc', 'val'],
545 data
: PVE
.Utils
.kvm_ostypes
.Linux
,
548 { xtype
: 'displayfield' }, // spacer
550 xtype
: 'PVE.form.BridgeSelector',
551 reference
: 'defaultBridge',
552 name
: 'defaultBridge',
554 fieldLabel
: gettext('Default Bridge'),
560 xtype
: 'proxmoxcheckbox',
561 fieldLabel
: gettext('Live Import'),
562 reference
: 'liveimport',
564 boxLabel
: gettext('Starts a previously stopped VM on Proxmox VE and imports the disks in the background.'),
566 value
: '{liveImport}',
570 xtype
: 'displayfield',
571 userCls
: 'pmx-hint black',
572 value
: gettext('Note: If anything goes wrong during the live-import, new data written by the VM may be lost.'),
574 hidden
: '{!liveImport}',
578 xtype
: 'displayfield',
579 fieldLabel
: gettext('Warnings'),
583 hidden
: '{hideWarnings}',
587 xtype
: 'displayfield',
588 reference
: 'warningText',
592 hidden
: '{hideWarnings}',
593 value
: '{warningsText}',
599 title
: gettext('Advanced'),
602 // the first inputpanel handles all values, so prevent value leakage here
603 onGetValues
: () => ({}),
607 xtype
: 'displayfield',
608 fieldLabel
: gettext('Disks'),
613 reference
: 'diskGrid',
624 xtype
: 'checkcolumn',
625 header
: gettext('Use'),
629 checkchange: function(_column
, _rowIndex
, _checked
, record
) {
635 text
: gettext('Disk'),
637 renderer
: 'renderDisk',
640 text
: gettext('Source'),
641 dataIndex
: 'import-from',
643 renderer: function(value
) {
644 return value
.replace(/^.*\//, '');
648 text
: gettext('Size'),
650 renderer
: (value
) => {
651 if (Ext
.isNumeric(value
)) {
652 return Proxmox
.Utils
.render_size(value
);
654 return value
?? Proxmox
.Utils
.unknownText
;
658 text
: gettext('Storage'),
660 xtype
: 'widgetcolumn',
663 xtype
: 'pveStorageSelector',
667 emptyText
: gettext('From Default'),
669 storageContent
: 'images',
671 onWidgetAttach
: 'setNodename',
674 text
: gettext('Format'),
676 xtype
: 'widgetcolumn',
679 xtype
: 'pveDiskFormatSelector',
683 matchFieldWidth
: false,
692 xtype
: 'proxmoxcheckbox',
693 boxLabel
: gettext('Prepare for VirtIO-SCSI'),
694 reference
: 'prepareForVirtIO',
695 name
: 'prepareForVirtIO',
699 disabled
: '{!isWindows}',
703 'data-qtip': gettext('Maps SCSI disks to SATA and changes the SCSI Controller. Useful for a quicker switch to VirtIO-SCSI attached disks'),
710 xtype
: 'pveScsiHwSelector',
713 value
: '__default__',
715 fieldLabel
: gettext('SCSI Controller'),
721 xtype
: 'displayfield',
722 fieldLabel
: gettext('CD/DVD Drives'),
737 return !rec
.data
.hidden
;
743 xtype
: 'checkcolumn',
744 header
: gettext('Use'),
748 checkchange: function(_column
, _rowIndex
, _checked
, record
) {
754 text
: gettext('Slot'),
759 text
: gettext('Storage'),
760 xtype
: 'widgetcolumn',
763 xtype
: 'pveStorageSelector',
767 emptyText
: Proxmox
.Utils
.noneText
,
768 storageContent
: 'iso',
770 onWidgetAttach
: 'setNodename',
773 text
: gettext('ISO'),
775 xtype
: 'widgetcolumn',
778 xtype
: 'pveFileSelector',
782 emptyText
: Proxmox
.Utils
.noneText
,
783 storageContent
: 'iso',
785 onWidgetAttach
: 'setNodename',
790 xtype
: 'displayfield',
791 fieldLabel
: gettext('Network Interfaces'),
801 reference
: 'netGrid',
810 xtype
: 'checkcolumn',
811 header
: gettext('Use'),
815 checkchange: function(_column
, _rowIndex
, _checked
, record
) {
825 text
: gettext('MAC address'),
827 dataIndex
: 'macaddr',
828 renderer
: 'renderMacAddress',
831 text
: gettext('Model'),
834 xtype
: 'widgetcolumn',
836 xtype
: 'pveNetworkCardSelector',
843 text
: gettext('Bridge'),
845 xtype
: 'widgetcolumn',
848 xtype
: 'PVE.form.BridgeSelector',
853 emptyText
: gettext('From Default'),
855 onWidgetAttach
: 'setNodename',
858 text
: gettext('VLAN Tag'),
860 xtype
: 'widgetcolumn',
863 xtype
: 'pveVlanField',
864 fieldLabel
: undefined,
873 xtype
: 'proxmoxcheckbox',
875 boxLabel
: gettext('Unique MAC addresses'),
876 uncheckedValue
: false,
882 title
: gettext('Resulting Config'),
883 reference
: 'summaryTab',
887 reference
: 'summaryGrid',
898 { header
: 'Key', width
: 150, dataIndex
: 'key' },
899 { header
: 'Value', flex
: 1, dataIndex
: 'value' },
907 initComponent: function() {
910 if (!me
.volumeName
) {
911 throw "no volumeName given";
915 throw "no storage given";
919 throw "no nodename given";
924 me
.setTitle(Ext
.String
.format(gettext('Import Guest - {0}'), `${me.storage}:${me.volumeName}`));
926 me
.lookup('defaultStorage').setNodename(me
.nodename
);
927 me
.lookup('defaultBridge').setNodename(me
.nodename
);
929 let renderWarning
= w
=> {
930 const warningsCatalogue
= {
931 'cdrom-image-ignored': gettext("CD-ROM images cannot get imported, if required you can reconfigure the '{0}' drive in the 'Advanced' tab."),
932 'nvme-unsupported': gettext("NVMe disks are currently not supported, '{0}' will get attaced as SCSI"),
933 'ovmf-with-lsi-unsupported': gettext("OVMF is built without LSI drivers, scsi hardware was set to '{1}'"),
934 'serial-port-socket-only': gettext("Serial socket '{0}' will be mapped to a socket"),
935 'guest-is-running': gettext('Virtual guest seems to be running on source host. Import might fail or have inconsistent state!'),
936 'efi-state-lost': Ext
.String
.format(
937 gettext('EFI state cannot be imported, you may need to reconfigure the boot order (see {0})'),
938 '<a href="https://pve.proxmox.com/wiki/OVMF/UEFI_Boot_Entries">OVMF/UEFI Boot Entries</a>',
941 let message
= warningsCatalogue
[w
.type
];
942 if (!w
.type
|| !message
) {
943 return w
.message
?? w
.type
?? gettext('Unknown warning');
945 return Ext
.String
.format(message
, w
.key
?? 'unknown', w
.value
?? 'unknown');
949 success: function(response
) {
950 let data
= response
.result
.data
;
951 me
.vmConfig
= data
['create-args'];
954 for (const [id
, value
] of Object
.entries(data
.disks
?? {})) {
955 let volid
= Ext
.htmlEncode('<none>');
957 if (Ext
.isObject(value
)) {
965 'import-from': volid
,
971 for (const [id
, parsed
] of Object
.entries(data
.net
?? {})) {
973 parsed
.enable
= true;
978 for (const [id
, value
] of Object
.entries(me
.vmConfig
)) {
979 if (!Ext
.isString(value
) || !value
.match(/media=cdrom/)) {
987 delete me
.vmConfig
[id
];
990 me
.lookup('diskGrid').getStore().setData(disks
);
991 me
.lookup('netGrid').getStore().setData(nets
);
992 me
.lookup('cdGrid').getStore().setData(cdroms
);
994 let additionalCdIdx
= me
.getController().calculateAdditionalCDIdx();
995 if (additionalCdIdx
=== '') {
996 me
.getViewModel().set('maxCdDrives', true);
997 } else if (cdroms
.length
=== 0) {
998 me
.additionalCdIdx
= additionalCdIdx
;
999 me
.lookup('cdGrid').getStore().add({
1001 hidden
: !(me
.vmConfig
.ostype
?? '').startsWith('w'),
1002 id
: additionalCdIdx
,
1003 autogenerated
: true,
1007 me
.getViewModel().set('warnings', data
.warnings
.map(w
=> renderWarning(w
)));
1009 let osinfo
= PVE
.Utils
.get_kvm_osinfo(me
.vmConfig
.ostype
?? '');
1010 let prepareForVirtIO
= (me
.vmConfig
.ostype
?? '').startsWith('w') && (me
.vmConfig
.bios
?? '').indexOf('ovmf') !== -1;
1013 osbase
: osinfo
.base
,
1018 me
.lookup('prepareForVirtIO').setValue(prepareForVirtIO
);