Ext.define('PVE.window.GuestImport', { extend: 'Proxmox.window.Edit', // fixme: Proxmox.window.Edit? alias: 'widget.pveGuestImportWindow', title: gettext('Import Guest'), onlineHelp: 'qm_import_virtual_machines', width: 720, bodyPadding: 0, submitUrl: function() { let me = this; return `/nodes/${me.nodename}/qemu`; }, isAdd: true, isCreate: true, submitText: gettext('Import'), showTaskViewer: true, method: 'POST', loadUrl: function(_url, { storage, nodename, volumeName }) { let args = Ext.Object.toQueryString({ volume: volumeName }); return `/nodes/${nodename}/storage/${storage}/import-metadata?${args}`; }, controller: { xclass: 'Ext.app.ViewController', setNodename: function(_column, widget) { let me = this; let view = me.getView(); widget.setNodename(view.nodename); }, diskStorageChange: function(storageSelector, value) { let me = this; let grid = me.lookup('diskGrid'); let rec = storageSelector.getWidgetRecord(); let validFormats = storageSelector.store.getById(value)?.data.format; grid.query('pveDiskFormatSelector').some((selector) => { if (selector.getWidgetRecord().data.id !== rec.data.id) { return false; } if (validFormats?.[0]?.qcow2) { selector.setDisabled(false); selector.setValue('qcow2'); } else { selector.setValue('raw'); selector.setDisabled(true); } return true; }); }, isoStorageChange: function(storageSelector, value) { let me = this; let grid = me.lookup('cdGrid'); let rec = storageSelector.getWidgetRecord(); grid.query('pveFileSelector').some((selector) => { if (selector.getWidgetRecord().data.id !== rec.data.id) { return false; } selector.setStorage(value); if (!value) { selector.setValue(''); } return true; }); }, onOSBaseChange: function(_field, value) { let me = this; let ostype = me.lookup('ostype'); let store = ostype.getStore(); store.setData(PVE.Utils.kvm_ostypes[value]); let old_val = ostype.getValue(); if (old_val && store.find('val', old_val) !== -1) { ostype.setValue(old_val); } else { ostype.setValue(store.getAt(0)); } }, calculateConfig: function() { let me = this; let inputPanel = me.lookup('mainInputPanel'); let summaryGrid = me.lookup('summaryGrid'); let values = inputPanel.getValues(); summaryGrid.getStore().setData(Object.entries(values).map(([key, value]) => ({ key, value }))); }, calculateAdditionalCDIdx: function() { let me = this; let maxIde = me.getMaxControllerId('ide'); let maxSata = me.getMaxControllerId('sata'); // only ide0 and ide2 can be used reliably for isos (e.g. for q35) if (maxIde < 0) { return 'ide0'; } if (maxIde < 2) { return 'ide2'; } if (maxSata < PVE.Utils.diskControllerMaxIDs.sata - 1) { return `sata${maxSata+1}`; } return ''; }, // assume assigned sata disks indices are continuous, so without holes getMaxControllerId: function(controller) { let me = this; let view = me.getView(); if (!controller) { return -1; } let max = view[`max${controller}`]; if (max !== undefined) { return max; } max = -1; for (const key of Object.keys(me.getView().vmConfig)) { if (!key.toLowerCase().startsWith(controller)) { continue; } let idx = parseInt(key.slice(controller.length), 10); if (idx > max) { max = idx; } } me.lookup('diskGrid').getStore().each(rec => { if (!rec.data.id.toLowerCase().startsWith(controller)) { return; } let idx = parseInt(rec.data.id.slice(controller.length), 10); if (idx > max) { max = idx; } }); me.lookup('cdGrid').getStore().each(rec => { if (!rec.data.id.toLowerCase().startsWith(controller) || rec.data.hidden) { return; } let idx = parseInt(rec.data.id.slice(controller.length), 10); if (idx > max) { max = idx; } }); view[`max${controller}`] = max; return max; }, renderDisk: function(value, metaData, record, rowIndex, colIndex, store, tableView) { let diskGrid = tableView.grid ?? this.lookup('diskGrid'); if (diskGrid.diskMap) { let mappedID = diskGrid.diskMap[value]; if (mappedID) { let prefix = ''; if (mappedID === value) { // mapped to the same value means we ran out of IDs let warning = gettext('Too many disks, could not map to SATA.'); prefix = ` `; } return `${prefix}${mappedID}`; } } return value; }, refreshGrids: function() { this.lookup('diskGrid').reconfigure(); this.lookup('cdGrid').reconfigure(); this.lookup('netGrid').reconfigure(); }, onOSTypeChange: function(_cb, value) { let me = this; if (!value) { return; } let store = me.lookup('cdGrid').getStore(); let collection = store.getData().getSource() ?? store.getData(); let rec = collection.find('autogenerated', true); let isWindows = (value ?? '').startsWith('w'); if (rec) { rec.set('hidden', !isWindows); rec.commit(); } let prepareVirtio = me.lookup('prepareForVirtIO').getValue(); let defaultScsiHw = me.getView().vmConfig.scsihw ?? '__default__'; me.lookup('scsihw').setValue(prepareVirtio && isWindows ? 'virtio-scsi-single' : defaultScsiHw); me.refreshGrids(); }, onPrepareVirtioChange: function(_cb, value) { let me = this; let view = me.getView(); let diskGrid = me.lookup('diskGrid'); diskGrid.diskMap = {}; if (value) { const hasAdditionalSataCDROM = me.getViewModel().get('isWindows') && view.additionalCdIdx?.startsWith('sata'); diskGrid.getStore().each(rec => { let diskID = rec.data.id; if (!diskID.toLowerCase().startsWith('scsi')) { return; // continue } let offset = parseInt(diskID.slice(4), 10); let newIdx = offset + me.getMaxControllerId('sata') + 1; if (hasAdditionalSataCDROM) { newIdx++; } let mappedID = `sata${newIdx}`; if (newIdx >= PVE.Utils.diskControllerMaxIDs.sata) { mappedID = diskID; // map to self so that the renderer can detect that we're out of IDs } diskGrid.diskMap[diskID] = mappedID; }); } let scsihw = me.lookup('scsihw'); scsihw.suspendEvents(); scsihw.setValue(value ? 'virtio-scsi-single' : me.getView().vmConfig.scsihw); scsihw.resumeEvents(); me.refreshGrids(); }, onScsiHwChange: function(_field, value) { let me = this; me.getView().vmConfig.scsihw = value; }, onUniqueMACChange: function(_cb, value) { let me = this; me.getViewModel().set('uniqueMACAdresses', value); me.lookup('netGrid').reconfigure(); }, renderMacAddress: function(value, metaData, record, rowIndex, colIndex, store, view) { let me = this; let vm = me.getViewModel(); return !vm.get('uniqueMACAdresses') && value ? value : 'auto'; }, control: { 'grid field': { // update records from widgetcolumns change: function(widget, value) { let rec = widget.getWidgetRecord(); rec.set(widget.name, value); rec.commit(); }, }, 'grid[reference=diskGrid] pveStorageSelector': { change: 'diskStorageChange', }, 'grid[reference=cdGrid] pveStorageSelector': { change: 'isoStorageChange', }, 'field[name=osbase]': { change: 'onOSBaseChange', }, 'panel[reference=summaryTab]': { activate: 'calculateConfig', }, 'proxmoxcheckbox[reference=prepareForVirtIO]': { change: 'onPrepareVirtioChange', }, 'combobox[name=ostype]': { change: 'onOSTypeChange', }, 'pveScsiHwSelector': { change: 'onScsiHwChange', }, 'proxmoxcheckbox[name=uniqueMACs]': { change: 'onUniqueMACChange', }, }, }, viewModel: { data: { coreCount: 1, socketCount: 1, liveImport: false, os: 'l26', maxCdDrives: false, uniqueMACAdresses: false, isOva: false, warnings: [], }, formulas: { totalCoreCount: get => get('socketCount') * get('coreCount'), hideWarnings: get => get('warnings').length === 0, warningsText: get => '', liveImportNote: get => !get('liveImport') ? '' : gettext('Note: If anything goes wrong during the live-import, new data written by the VM may be lost.'), isWindows: get => (get('os') ?? '').startsWith('w'), liveImportText: get => get('isOva') ? gettext('Starts a VM and imports the disks in the background') : gettext('Starts a previously stopped VM on Proxmox VE and imports the disks in the background.'), }, }, items: [{ xtype: 'tabpanel', defaults: { bodyPadding: 10, }, items: [ { title: gettext('General'), xtype: 'inputpanel', reference: 'mainInputPanel', onGetValues: function(values) { let me = this; let view = me.up('pveGuestImportWindow'); let vm = view.getViewModel(); let diskGrid = view.lookup('diskGrid'); // from pveDiskStorageSelector let defaultStorage = values.hdstorage; let defaultFormat = values.diskformat; delete values.hdstorage; delete values.diskformat; let defaultBridge = values.defaultBridge; delete values.defaultBridge; let config = { ...view.vmConfig }; Ext.apply(config, values); if (config.scsi0) { config.scsi0 = config.scsi0.replace('local:0,', 'local:0,format=qcow2,'); } let parsedBoot = PVE.Parser.parsePropertyString(config.boot ?? ''); if (parsedBoot.order) { parsedBoot.order = parsedBoot.order.split(';'); } let diskMap = diskGrid.diskMap ?? {}; diskGrid.getStore().each(rec => { if (!rec.data.enable) { return; } let id = diskMap[rec.data.id] ?? rec.data.id; if (id !== rec.data.id && parsedBoot?.order) { let idx = parsedBoot.order.indexOf(rec.data.id); if (idx !== -1) { parsedBoot.order[idx] = id; } } let data = { ...rec.data, }; delete data.enable; delete data.id; delete data.size; if (!data.file) { data.file = defaultStorage; data.format = defaultFormat; } data.file += ':0'; // for our special api format if (id === 'efidisk0') { delete data['import-from']; } config[id] = PVE.Parser.printQemuDrive(data); }); if (parsedBoot.order) { parsedBoot.order = parsedBoot.order.join(';'); } config.boot = PVE.Parser.printPropertyString(parsedBoot); view.lookup('netGrid').getStore().each((rec) => { if (!rec.data.enable) { return; } let id = rec.data.id; let data = { ...rec.data, }; delete data.enable; delete data.id; if (!data.bridge) { data.bridge = defaultBridge; } if (vm.get('uniqueMACAdresses')) { data.macaddr = undefined; } config[id] = PVE.Parser.printQemuNetwork(data); }); view.lookup('cdGrid').getStore().each((rec) => { if (!rec.data.enable) { return; } let id = rec.data.id; let cd = { media: 'cdrom', file: rec.data.file ? rec.data.file : 'none', }; config[id] = PVE.Parser.printPropertyString(cd); }); config.scsihw = view.lookup('scsihw').getValue(); if (view.lookup('liveimport').getValue()) { config['live-restore'] = 1; } // remove __default__ values for (const [key, value] of Object.entries(config)) { if (value === '__default__') { delete config[key]; } } if (config['import-working-storage'] === '') { delete config['import-working-storage']; } return config; }, column1: [ { xtype: 'pveGuestIDSelector', name: 'vmid', fieldLabel: 'VM', guestType: 'qemu', loadNextFreeID: true, validateExists: false, }, { xtype: 'proxmoxintegerfield', fieldLabel: gettext('Sockets'), name: 'sockets', reference: 'socketsField', value: 1, minValue: 1, maxValue: 128, allowBlank: true, bind: { value: '{socketCount}', }, }, { xtype: 'proxmoxintegerfield', fieldLabel: gettext('Cores'), name: 'cores', reference: 'coresField', value: 1, minValue: 1, maxValue: 1024, allowBlank: true, bind: { value: '{coreCount}', }, }, { xtype: 'pveMemoryField', fieldLabel: gettext('Memory') + ' (MiB)', name: 'memory', reference: 'memoryField', value: 512, allowBlank: true, }, { xtype: 'displayfield' }, // spacer { xtype: 'displayfield' }, // spacer { xtype: 'pveDiskStorageSelector', reference: 'defaultStorage', storageLabel: gettext('Default Storage'), storageContent: 'images', autoSelect: true, hideSize: true, name: 'defaultStorage', }, ], column2: [ { xtype: 'textfield', fieldLabel: gettext('Name'), name: 'name', vtype: 'DnsName', reference: 'nameField', allowBlank: true, }, { xtype: 'CPUModelSelector', name: 'cpu', reference: 'cputype', value: 'x86-64-v2-AES', fieldLabel: gettext('CPU Type'), }, { xtype: 'displayfield', fieldLabel: gettext('Total cores'), name: 'totalcores', isFormField: false, bind: { value: '{totalCoreCount}', }, }, { xtype: 'combobox', submitValue: false, name: 'osbase', fieldLabel: gettext('OS Type'), editable: false, queryMode: 'local', value: 'Linux', store: Object.keys(PVE.Utils.kvm_ostypes), }, { xtype: 'combobox', name: 'ostype', reference: 'ostype', fieldLabel: gettext('Version'), value: 'l26', allowBlank: false, editable: false, queryMode: 'local', valueField: 'val', displayField: 'desc', bind: { value: '{os}', }, store: { fields: ['desc', 'val'], data: PVE.Utils.kvm_ostypes.Linux, }, }, { xtype: 'displayfield' }, // spacer { xtype: 'PVE.form.BridgeSelector', reference: 'defaultBridge', name: 'defaultBridge', allowBlank: false, fieldLabel: gettext('Default Bridge'), }, { xtype: 'pveStorageSelector', reference: 'extractionStorage', fieldLabel: gettext('Import Working Storage'), storageContent: 'images', emptyText: gettext('Source Storage'), autoSelect: false, name: 'import-working-storage', disabled: true, hidden: true, allowBlank: true, bind: { disabled: '{!isOva}', hidden: '{!isOva}', }, }, ], columnB: [ { xtype: 'proxmoxcheckbox', fieldLabel: gettext('Live Import'), reference: 'liveimport', isFormField: false, bind: { value: '{liveImport}', boxLabel: '{liveImportText}', }, }, { xtype: 'displayfield', userCls: 'pmx-hint black', value: gettext('Note: If anything goes wrong during the live-import, new data written by the VM may be lost.'), bind: { hidden: '{!liveImport}', }, }, { xtype: 'displayfield', fieldLabel: gettext('Warnings'), labelWidth: 200, hidden: true, bind: { hidden: '{hideWarnings}', }, }, { xtype: 'displayfield', reference: 'warningText', userCls: 'pmx-hint', hidden: true, bind: { hidden: '{hideWarnings}', value: '{warningsText}', }, }, ], }, { title: gettext('Advanced'), xtype: 'inputpanel', // the first inputpanel handles all values, so prevent value leakage here onGetValues: () => ({}), columnT: [ { xtype: 'displayfield', fieldLabel: gettext('Disks'), labelWidth: 200, }, { xtype: 'grid', reference: 'diskGrid', minHeight: 60, maxHeight: 150, store: { data: [], sorters: [ 'id', ], }, columns: [ { xtype: 'checkcolumn', header: gettext('Use'), width: 50, dataIndex: 'enable', listeners: { checkchange: function(_column, _rowIndex, _checked, record) { record.commit(); }, }, }, { text: gettext('Disk'), dataIndex: 'id', renderer: 'renderDisk', }, { text: gettext('Source'), dataIndex: 'import-from', flex: 1, renderer: function(value) { return value.replace(/^.*\//, ''); }, }, { text: gettext('Size'), dataIndex: 'size', renderer: (value) => { if (Ext.isNumeric(value)) { return Proxmox.Utils.render_size(value); } return value ?? Proxmox.Utils.unknownText; }, }, { text: gettext('Storage'), dataIndex: 'file', xtype: 'widgetcolumn', width: 150, widget: { xtype: 'pveStorageSelector', isFormField: false, autoSelect: false, allowBlank: true, emptyText: gettext('From Default'), name: 'file', storageContent: 'images', }, onWidgetAttach: 'setNodename', }, { text: gettext('Format'), dataIndex: 'format', xtype: 'widgetcolumn', width: 150, widget: { xtype: 'pveDiskFormatSelector', name: 'format', disabled: true, isFormField: false, matchFieldWidth: false, }, }, ], }, ], column1: [ { xtype: 'proxmoxcheckbox', boxLabel: gettext('Prepare for VirtIO-SCSI'), reference: 'prepareForVirtIO', name: 'prepareForVirtIO', submitValue: false, disabled: true, bind: { disabled: '{!isWindows}', }, autoEl: { tag: 'div', 'data-qtip': gettext('Maps SCSI disks to SATA and changes the SCSI Controller. Useful for a quicker switch to VirtIO-SCSI attached disks'), }, }, ], column2: [ { xtype: 'pveScsiHwSelector', reference: 'scsihw', name: 'scsihw', value: '__default__', submitValue: false, fieldLabel: gettext('SCSI Controller'), }, ], columnB: [ { xtype: 'displayfield', fieldLabel: gettext('CD/DVD Drives'), labelWidth: 200, }, { xtype: 'grid', reference: 'cdGrid', minHeight: 60, maxHeight: 150, store: { data: [], sorters: [ 'id', ], filters: [ function(rec) { return !rec.data.hidden; }, ], }, columns: [ { xtype: 'checkcolumn', header: gettext('Use'), width: 50, dataIndex: 'enable', listeners: { checkchange: function(_column, _rowIndex, _checked, record) { record.commit(); }, }, }, { text: gettext('Slot'), dataIndex: 'id', sorted: true, }, { text: gettext('Storage'), xtype: 'widgetcolumn', width: 150, widget: { xtype: 'pveStorageSelector', isFormField: false, autoSelect: false, allowBlank: true, emptyText: Proxmox.Utils.noneText, storageContent: 'iso', }, onWidgetAttach: 'setNodename', }, { text: gettext('ISO'), dataIndex: 'file', xtype: 'widgetcolumn', flex: 1, widget: { xtype: 'pveFileSelector', name: 'file', isFormField: false, allowBlank: true, emptyText: Proxmox.Utils.noneText, storageContent: 'iso', }, onWidgetAttach: 'setNodename', }, ], }, { xtype: 'displayfield', fieldLabel: gettext('Network Interfaces'), labelWidth: 200, style: { paddingTop: '10px', }, }, { xtype: 'grid', minHeight: 58, maxHeight: 150, reference: 'netGrid', store: { data: [], sorters: [ 'id', ], }, columns: [ { xtype: 'checkcolumn', header: gettext('Use'), width: 50, dataIndex: 'enable', listeners: { checkchange: function(_column, _rowIndex, _checked, record) { record.commit(); }, }, }, { text: gettext('ID'), dataIndex: 'id', }, { text: gettext('MAC address'), flex: 7, dataIndex: 'macaddr', renderer: 'renderMacAddress', }, { text: gettext('Model'), flex: 7, dataIndex: 'model', xtype: 'widgetcolumn', widget: { xtype: 'pveNetworkCardSelector', name: 'model', isFormField: false, allowBlank: false, }, }, { text: gettext('Bridge'), dataIndex: 'bridge', xtype: 'widgetcolumn', flex: 6, widget: { xtype: 'PVE.form.BridgeSelector', name: 'bridge', isFormField: false, autoSelect: false, allowBlank: true, emptyText: gettext('From Default'), }, onWidgetAttach: 'setNodename', }, { text: gettext('VLAN Tag'), dataIndex: 'tag', xtype: 'widgetcolumn', flex: 5, widget: { xtype: 'pveVlanField', fieldLabel: undefined, name: 'tag', isFormField: false, allowBlank: true, }, }, ], }, { xtype: 'proxmoxcheckbox', name: 'uniqueMACs', boxLabel: gettext('Unique MAC addresses'), uncheckedValue: false, value: false, }, ], }, { title: gettext('Resulting Config'), reference: 'summaryTab', items: [ { xtype: 'grid', reference: 'summaryGrid', maxHeight: 400, scrollable: true, store: { model: 'KeyValue', sorters: [{ property: 'key', direction: 'ASC', }], }, columns: [ { header: 'Key', width: 150, dataIndex: 'key' }, { header: 'Value', flex: 1, dataIndex: 'value' }, ], }, ], }, ], }], initComponent: function() { let me = this; if (!me.volumeName) { throw "no volumeName given"; } if (!me.storage) { throw "no storage given"; } if (!me.nodename) { throw "no nodename given"; } me.callParent(); me.setTitle(Ext.String.format(gettext('Import Guest - {0}'), `${me.storage}:${me.volumeName}`)); me.lookup('defaultStorage').setNodename(me.nodename); me.lookup('defaultBridge').setNodename(me.nodename); me.lookup('extractionStorage').setNodename(me.nodename); let renderWarning = w => { const warningsCatalogue = { 'cdrom-image-ignored': gettext("CD-ROM images cannot get imported, if required you can reconfigure the '{0}' drive in the 'Advanced' tab."), 'nvme-unsupported': gettext("NVMe disks are currently not supported, '{0}' will get attached as SCSI"), 'ovmf-with-lsi-unsupported': gettext("OVMF is built without LSI drivers, scsi hardware was set to '{1}'"), 'serial-port-socket-only': gettext("Serial socket '{0}' will be mapped to a socket"), 'guest-is-running': gettext('Virtual guest seems to be running on source host. Import might fail or have inconsistent state!'), 'efi-state-lost': Ext.String.format( gettext('EFI state cannot be imported, you may need to reconfigure the boot order (see {0})'), 'OVMF/UEFI Boot Entries', ), 'ova-needs-extracting': gettext('Importing an OVA temporarily requires extra space on the working storage while extracting the contained disks for further processing.'), }; let message = warningsCatalogue[w.type]; if (!w.type || !message) { return w.message ?? w.type ?? gettext('Unknown warning'); } return Ext.String.format(message, w.key ?? 'unknown', w.value ?? 'unknown'); }; me.load({ success: function(response) { let data = response.result.data; me.vmConfig = data['create-args']; let disks = []; for (const [id, value] of Object.entries(data.disks ?? {})) { let volid = Ext.htmlEncode(''); let size = 'auto'; if (Ext.isObject(value)) { volid = value.volid; size = value.size; } disks.push({ id, enable: true, size, 'import-from': volid, format: 'raw', }); } let nets = []; for (const [id, parsed] of Object.entries(data.net ?? {})) { parsed.id = id; parsed.enable = true; nets.push(parsed); } let cdroms = []; for (const [id, value] of Object.entries(me.vmConfig)) { if (!Ext.isString(value) || !value.match(/media=cdrom/)) { continue; } cdroms.push({ enable: true, hidden: false, id, }); delete me.vmConfig[id]; } me.lookup('diskGrid').getStore().setData(disks); me.lookup('netGrid').getStore().setData(nets); me.lookup('cdGrid').getStore().setData(cdroms); let additionalCdIdx = me.getController().calculateAdditionalCDIdx(); if (additionalCdIdx === '') { me.getViewModel().set('maxCdDrives', true); } else if (cdroms.length === 0) { me.additionalCdIdx = additionalCdIdx; me.lookup('cdGrid').getStore().add({ enable: true, hidden: !(me.vmConfig.ostype ?? '').startsWith('w'), id: additionalCdIdx, autogenerated: true, }); } me.getViewModel().set('warnings', data.warnings.map(w => renderWarning(w))); me.getViewModel().set('isOva', data.warnings.map(w => w.type).indexOf('ova-needs-extracting') !== -1); let osinfo = PVE.Utils.get_kvm_osinfo(me.vmConfig.ostype ?? ''); let prepareForVirtIO = (me.vmConfig.ostype ?? '').startsWith('w') && (me.vmConfig.bios ?? '').indexOf('ovmf') !== -1; me.setValues({ osbase: osinfo.base, ...me.vmConfig, }); me.lookup('prepareForVirtIO').setValue(prepareForVirtIO); }, }); }, });