X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=www%2Fmanager6%2Fqemu%2FHardwareView.js;h=af35a980f88d4e8a9f211912af3eee392538ce25;hb=refs%2Fheads%2Fmaster;hp=6bbc090c64c965ca1f128104aa432222a66132e6;hpb=fa522e4867de35e8707013cf6885f5d1255682d6;p=pve-manager.git diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js index 6bbc090c..672a7e1a 100644 --- a/www/manager6/qemu/HardwareView.js +++ b/www/manager6/qemu/HardwareView.js @@ -1,7 +1,5 @@ -// fixme: howto avoid jslint type confusion? -/*jslint confusion: true */ Ext.define('PVE.qemu.HardwareView', { - extend: 'PVE.grid.PendingObjectGrid', + extend: 'Proxmox.grid.PendingObjectGrid', alias: ['widget.PVE.qemu.HardwareView'], onlineHelp: 'qm_virtual_machines_settings', @@ -10,65 +8,103 @@ Ext.define('PVE.qemu.HardwareView', { var me = this; var rows = me.rows; var rowdef = rows[key] || {}; + var iconCls = rowdef.iconCls; + var icon = ''; + var txt = rowdef.header || key; metaData.tdAttr = "valign=middle"; + if (rowdef.isOnStorageBus) { + var value = me.getObjectValue(key, '', false); + if (value === '') { + value = me.getObjectValue(key, '', true); + } + if (value.match(/vm-.*-cloudinit/)) { + iconCls = 'cloud'; + txt = rowdef.cloudheader; + } else if (value.match(/media=cdrom/)) { + metaData.tdCls = 'pve-itype-icon-cdrom'; + return rowdef.cdheader; + } + } + if (rowdef.tdCls) { metaData.tdCls = rowdef.tdCls; - if (rowdef.tdCls == 'pve-itype-icon-storage') { - var value = me.getObjectValue(key, '', true); - if (value.match(/media=cdrom/)) { - metaData.tdCls = 'pve-itype-icon-cdrom'; - return rowdef.cdheader; - } - } + } else if (iconCls) { + icon = ""; + metaData.tdCls += " pve-itype-fa"; + } + + // only return icons in grid but not remove dialog + if (rowIndex !== undefined) { + return icon + txt; + } else { + return txt; } - return rowdef.header || key; }, - initComponent : function() { + initComponent: function() { var me = this; - var i, confid; - var nodename = me.pveSelNode.data.node; - if (!nodename) { + const { node: nodename, vmid } = me.pveSelNode.data; + if (!nodename) { throw "no node name specified"; - } - - var vmid = me.pveSelNode.data.vmid; - if (!vmid) { + } else if (!vmid) { throw "no VM ID specified"; } - var caps = Ext.state.Manager.get('GuiCap'); + const caps = Ext.state.Manager.get('GuiCap'); + const diskCap = caps.vms['VM.Config.Disk']; + const cdromCap = caps.vms['VM.Config.CDROM']; + + let isCloudInitKey = v => v && v.toString().match(/vm-.*-cloudinit/); - var rows = { + const nodeInfo = PVE.data.ResourceStore.getNodes().find(node => node.node === nodename); + let processorEditor = { + xtype: 'pveQemuProcessorEdit', + cgroupMode: nodeInfo['cgroup-mode'], + }; + + let rows = { memory: { header: gettext('Memory'), editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined, never_delete: true, - defaultValue: 512, + defaultValue: '512', tdCls: 'pve-itype-icon-memory', - renderer: function(value, metaData, record) { - var balloon = me.getObjectValue('balloon'); - if (balloon) { - return PVE.Utils.format_size(balloon*1024*1024) + "/" + - PVE.Utils.format_size(value*1024*1024); - - } - return PVE.Utils.format_size(value*1024*1024); - } + group: 2, + multiKey: ['memory', 'balloon', 'shares'], + renderer: function(value, metaData, record, ri, ci, store, pending) { + var res = ''; + + var max = me.getObjectValue('memory', 512, pending); + var balloon = me.getObjectValue('balloon', undefined, pending); + var shares = me.getObjectValue('shares', undefined, pending); + + res = Proxmox.Utils.format_size(max*1024*1024); + + if (balloon !== undefined && balloon > 0) { + res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res; + + if (shares) { + res += ' [shares=' + shares +']'; + } + } else if (balloon === 0) { + res += ' [balloon=0]'; + } + return res; + }, }, sockets: { header: gettext('Processors'), never_delete: true, - editor: (caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']) ? - 'PVE.qemu.ProcessorEdit' : undefined, - tdCls: 'pve-itype-icon-processor', - defaultValue: 1, + editor: caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType'] + ? processorEditor : undefined, + tdCls: 'pve-itype-icon-cpu', + group: 3, + defaultValue: '1', multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'], renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) { - var sockets = me.getObjectValue('sockets', 1, pending); var model = me.getObjectValue('cpu', undefined, pending); var cores = me.getObjectValue('cores', 1, pending); @@ -77,512 +113,649 @@ Ext.define('PVE.qemu.HardwareView', { var cpulimit = me.getObjectValue('cpulimit', undefined, pending); var cpuunits = me.getObjectValue('cpuunits', undefined, pending); - var res = (sockets*cores) + ' (' + sockets + ' sockets, ' + cores + ' cores)'; - + let res = Ext.String.format( + '{0} ({1} sockets, {2} cores)', sockets * cores, sockets, cores); + if (model) { res += ' [' + model + ']'; } - if (numa) { res += ' [numa=' + numa +']'; } - if (vcpus) { res += ' [vcpus=' + vcpus +']'; } - if (cpulimit) { res += ' [cpulimit=' + cpulimit +']'; } - if (cpuunits) { res += ' [cpuunits=' + cpuunits +']'; } return res; - } + }, }, - keyboard: { - header: gettext('Keyboard Layout'), + bios: { + header: 'BIOS', + group: 4, never_delete: true, - editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.KeyboardEdit' : undefined, - tdCls: 'pve-itype-icon-keyboard', + editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined, defaultValue: '', - renderer: PVE.Utils.render_kvm_language + iconCls: 'microchip', + renderer: PVE.Utils.render_qemu_bios, }, vga: { header: gettext('Display'), editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined, never_delete: true, - tdCls: 'pve-itype-icon-display', + iconCls: 'desktop', + group: 5, + defaultValue: '', + renderer: PVE.Utils.render_kvm_vga_driver, + }, + machine: { + header: gettext('Machine'), + editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.MachineEdit' : undefined, + iconCls: 'cogs', + never_delete: true, + group: 6, defaultValue: '', - renderer: PVE.Utils.render_kvm_vga_driver + renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) { + let ostype = me.getObjectValue('ostype', undefined, pending); + if (PVE.Utils.is_windows(ostype) && + (!value || value === 'pc' || value === 'q35')) { + return value === 'q35' ? 'pc-q35-5.1' : 'pc-i440fx-5.1'; + } + return PVE.Utils.render_qemu_machine(value); + }, + }, + scsihw: { + header: gettext('SCSI Controller'), + iconCls: 'database', + editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined, + renderer: PVE.Utils.render_scsihw, + group: 7, + never_delete: true, + defaultValue: '', + }, + vmstate: { + header: gettext('Hibernation VM State'), + iconCls: 'download', + del_extra_msg: gettext('The saved VM state will be permanently lost.'), + group: 100, }, cores: { - visible: false + visible: false, }, cpu: { - visible: false + visible: false, }, numa: { - visible: false + visible: false, }, balloon: { - visible: false + visible: false, }, hotplug: { - visible: false + visible: false, }, vcpus: { - visible: false + visible: false, }, cpuunits: { - visible: false + visible: false, }, cpulimit: { - visible: false + visible: false, + }, + shares: { + visible: false, + }, + ostype: { + visible: false, }, - bios: { - visible: false - } - }; - for (i = 0; i < 4; i++) { - confid = "ide" + i; + PVE.Utils.forEachBus(undefined, function(type, id) { + let confid = type + id; rows[confid] = { - group: 1, - tdCls: 'pve-itype-icon-storage', + group: 10, + iconCls: 'hdd-o', editor: 'PVE.qemu.HDEdit', - never_delete: caps.vms['VM.Config.Disk'] ? false : true, + isOnStorageBus: true, header: gettext('Hard Disk') + ' (' + confid +')', - cdheader: gettext('CD/DVD Drive') + ' (' + confid +')' + cdheader: gettext('CD/DVD Drive') + ' (' + confid +')', + cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')', }; - } - for (i = 0; i < 6; i++) { - confid = "sata" + i; + }); + for (let i = 0; i < PVE.Utils.hardware_counts.net; i++) { + let confid = "net" + i.toString(); rows[confid] = { - group: 1, - tdCls: 'pve-itype-icon-storage', - editor: 'PVE.qemu.HDEdit', - never_delete: caps.vms['VM.Config.Disk'] ? false : true, - header: gettext('Hard Disk') + ' (' + confid +')', - cdheader: gettext('CD/DVD Drive') + ' (' + confid +')' + group: 15, + order: i, + iconCls: 'exchange', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined, + never_delete: !caps.vms['VM.Config.Network'], + header: gettext('Network Device') + ' (' + confid +')', }; } - for (i = 0; i < 16; i++) { - confid = "scsi" + i; + rows.efidisk0 = { + group: 20, + iconCls: 'hdd-o', + editor: null, + never_delete: !caps.vms['VM.Config.Disk'], + header: gettext('EFI Disk'), + }; + rows.tpmstate0 = { + group: 22, + iconCls: 'hdd-o', + editor: null, + never_delete: !caps.vms['VM.Config.Disk'], + header: gettext('TPM State'), + }; + for (let i = 0; i < PVE.Utils.hardware_counts.usb; i++) { + let confid = "usb" + i.toString(); rows[confid] = { - group: 1, - tdCls: 'pve-itype-icon-storage', - editor: 'PVE.qemu.HDEdit', - never_delete: caps.vms['VM.Config.Disk'] ? false : true, - header: gettext('Hard Disk') + ' (' + confid +')', - cdheader: gettext('CD/DVD Drive') + ' (' + confid +')' + group: 25, + order: i, + iconCls: 'usb', + editor: caps.nodes['Sys.Console'] || caps.mapping['Mapping.Use'] ? 'PVE.qemu.USBEdit' : undefined, + never_delete: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], + header: gettext('USB Device') + ' (' + confid + ')', }; } - for (i = 0; i < 16; i++) { - confid = "virtio" + i; + for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) { + let confid = "hostpci" + i.toString(); rows[confid] = { - group: 1, - tdCls: 'pve-itype-icon-storage', - editor: 'PVE.qemu.HDEdit', - never_delete: caps.vms['VM.Config.Disk'] ? false : true, - header: gettext('Hard Disk') + ' (' + confid +')', - cdheader: gettext('CD/DVD Drive') + ' (' + confid +')' + group: 30, + order: i, + tdCls: 'pve-itype-icon-pci', + never_delete: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], + editor: caps.nodes['Sys.Console'] || caps.mapping['Mapping.Use'] ? 'PVE.qemu.PCIEdit' : undefined, + header: gettext('PCI Device') + ' (' + confid + ')', }; } - for (i = 0; i < 32; i++) { - confid = "net" + i; + for (let i = 0; i < PVE.Utils.hardware_counts.serial; i++) { + let confid = "serial" + i.toString(); rows[confid] = { - group: 2, - tdCls: 'pve-itype-icon-network', - editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined, - never_delete: caps.vms['VM.Config.Network'] ? false : true, - header: gettext('Network Device') + ' (' + confid +')' + group: 35, + order: i, + tdCls: 'pve-itype-icon-serial', + never_delete: !caps.nodes['Sys.Console'], + header: gettext('Serial Port') + ' (' + confid + ')', }; } - rows.efidisk0 = { - group: 3, - tdCls: 'pve-itype-icon-storage', - editor: null, - never_delete: caps.vms['VM.Config.Disk'] ? false : true, - header: gettext('EFI Disk') + rows.audio0 = { + group: 40, + iconCls: 'volume-up', + editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined, + never_delete: !caps.vms['VM.Config.HWType'], + header: gettext('Audio Device'), }; - for (i = 0; i < 5; i++) { - confid = "usb" + i; - rows[confid] = { - group: 4, - tdCls: 'pve-itype-icon-usb', - never_delete: true, - header: gettext('USB Device') + ' (' + confid + ')' - }; - } - for (i = 0; i < 8; i++) { - rows["unused" + i] = { - group: 5, - tdCls: 'pve-itype-icon-storage', + for (let i = 0; i < 256; i++) { + rows["unused" + i.toString()] = { + group: 99, + order: i, + iconCls: 'hdd-o', + del_extra_msg: gettext('This will permanently erase all data.'), editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined, - header: gettext('Unused Disk') + ' ' + i + header: gettext('Unused Disk') + ' ' + i.toString(), }; } + rows.rng0 = { + group: 45, + tdCls: 'pve-itype-icon-die', + editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.RNGEdit' : undefined, + never_delete: !caps.nodes['Sys.Console'], + header: gettext("VirtIO RNG"), + }; var sorterFn = function(rec1, rec2) { var v1 = rec1.data.key; var v2 = rec2.data.key; var g1 = rows[v1].group || 0; var g2 = rows[v2].group || 0; - - return (g1 !== g2) ? - (g1 > g2 ? 1 : -1) : (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)); - }; + var order1 = rows[v1].order || 0; + var order2 = rows[v2].order || 0; - var reload = function() { - me.rstore.load(); - }; - - var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config'; - - var sm = Ext.create('Ext.selection.RowModel', {}); - - var run_editor = function() { - var rec = sm.getSelection()[0]; - if (!rec) { - return; + if (g1 - g2 !== 0) { + return g1 - g2; } - var rowdef = rows[rec.data.key]; - if (!rowdef.editor) { - return; + if (order1 - order2 !== 0) { + return order1 - order2; } - var editor = rowdef.editor; - if (rowdef.tdCls == 'pve-itype-icon-storage') { - var value = me.getObjectValue(rec.data.key, '', true); - if (value.match(/media=cdrom/)) { - editor = 'PVE.qemu.CDEdit'; - } - } - - var win; - - if (Ext.isString(editor)) { - win = Ext.create(editor, { - pveSelNode: me.pveSelNode, - confid: rec.data.key, - url: '/api2/extjs/' + baseurl - }); + if (v1 > v2) { + return 1; + } else if (v1 < v2) { + return -1; } else { - var config = Ext.apply({ - pveSelNode: me.pveSelNode, - confid: rec.data.key, - url: '/api2/extjs/' + baseurl - }, rowdef.editor); - win = Ext.createWidget(rowdef.editor.xtype, config); - win.load(); + return 0; } - - win.show(); - win.on('destroy', reload); }; - var run_diskthrottle = function() { - var rec = sm.getSelection()[0]; - if (!rec) { - return; - } - - var win = Ext.create('PVE.qemu.HDThrottle', { - pveSelNode: me.pveSelNode, - confid: rec.data.key, - url: '/api2/extjs/' + baseurl - }); + let baseurl = `nodes/${nodename}/qemu/${vmid}/config`; - win.show(); - win.on('destroy', reload); - }; + let sm = Ext.create('Ext.selection.RowModel', {}); - var run_resize = function() { - var rec = sm.getSelection()[0]; - if (!rec) { + let run_editor = function() { + let rec = sm.getSelection()[0]; + if (!rec || !rows[rec.data.key]?.editor) { return; } + let rowdef = rows[rec.data.key]; + let editor = rowdef.editor; + + if (rowdef.isOnStorageBus) { + let value = me.getObjectValue(rec.data.key, '', true); + if (isCloudInitKey(value)) { + return; + } else if (value.match(/media=cdrom/)) { + editor = 'PVE.qemu.CDEdit'; + } else if (!diskCap) { + return; + } + } - var win = Ext.create('PVE.window.HDResize', { - disk: rec.data.key, - nodename: nodename, - vmid: vmid - }); - - win.show(); - - win.on('destroy', reload); - }; - - var run_cpuoptions = function() { - var sockets = me.getObjectValue('sockets', 1); - var cores = me.getObjectValue('cores', 1); - - var win = Ext.create('PVE.qemu.CPUOptions', { - maxvcpus: sockets * cores, - vmid: vmid, + let commonOpts = { + autoShow: true, pveSelNode: me.pveSelNode, - url: '/api2/extjs/' + baseurl - }); - - win.show(); - - win.on('destroy', reload); - }; + confid: rec.data.key, + url: `/api2/extjs/${baseurl}`, + listeners: { + destroy: () => me.reload(), + }, + }; - var run_move = function() { - var rec = sm.getSelection()[0]; - if (!rec) { - return; + if (Ext.isString(editor)) { + Ext.create(editor, commonOpts); + } else { + let win = Ext.createWidget(rowdef.editor.xtype, Ext.apply(commonOpts, rowdef.editor)); + win.load(); } - - var win = Ext.create('PVE.window.HDMove', { - disk: rec.data.key, - nodename: nodename, - vmid: vmid - }); - - win.show(); - - win.on('destroy', reload); }; - var edit_btn = new PVE.button.Button({ + let edit_btn = new Proxmox.button.Button({ text: gettext('Edit'), selModel: sm, disabled: true, - handler: run_editor - }); + handler: run_editor, + }); - var resize_btn = new PVE.button.Button({ - text: gettext('Resize disk'), + let move_menuitem = new Ext.menu.Item({ + text: gettext('Move Storage'), + tooltip: gettext('Move disk to another storage'), + iconCls: 'fa fa-database', selModel: sm, - disabled: true, - handler: run_resize + handler: () => { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + Ext.create('PVE.window.HDMove', { + autoShow: true, + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + type: 'qemu', + listeners: { + destroy: () => me.reload(), + }, + }); + }, }); - var move_btn = new PVE.button.Button({ - text: gettext('Move disk'), + let reassign_menuitem = new Ext.menu.Item({ + text: gettext('Reassign Owner'), + tooltip: gettext('Reassign disk to another VM'), + iconCls: 'fa fa-desktop', selModel: sm, - disabled: true, - handler: run_move + handler: () => { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + Ext.create('PVE.window.GuestDiskReassign', { + autoShow: true, + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + type: 'qemu', + listeners: { + destroy: () => me.reload(), + }, + }); + }, }); - var diskthrottle_btn = new PVE.button.Button({ - text: gettext('Disk Throttle'), + let resize_menuitem = new Ext.menu.Item({ + text: gettext('Resize'), + iconCls: 'fa fa-plus', selModel: sm, - disabled: true, - handler: run_diskthrottle + handler: () => { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + Ext.create('PVE.window.HDResize', { + autoShow: true, + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + listeners: { + destroy: () => me.reload(), + }, + }); + }, }); - var cpuoptions_btn = new Ext.Button({ - text: gettext('CPU options'), - handler: run_cpuoptions + let diskaction_btn = new Proxmox.button.Button({ + text: gettext('Disk Action'), + disabled: true, + menu: { + items: [ + move_menuitem, + reassign_menuitem, + resize_menuitem, + ], + }, }); - var remove_btn = new PVE.button.Button({ + + let remove_btn = new Proxmox.button.Button({ text: gettext('Remove'), + defaultText: gettext('Remove'), + altText: gettext('Detach'), selModel: sm, disabled: true, dangerous: true, + RESTMethod: 'PUT', confirmMsg: function(rec) { - var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), - "'" + me.renderKey(rec.data.key, {}, rec) + "'"); - if (rec.data.key.match(/^unused\d+$/)) { - msg += " " + gettext('This will permanently erase all data.'); + let warn = gettext('Are you sure you want to remove entry {0}'); + if (this.text === this.altText) { + warn = gettext('Are you sure you want to detach entry {0}'); } + let rendered = me.renderKey(rec.data.key, {}, rec); + let msg = Ext.String.format(warn, `'${rendered}'`); + if (rows[rec.data.key].del_extra_msg) { + msg += '
' + rows[rec.data.key].del_extra_msg; + } return msg; }, - handler: function(b, e, rec) { - PVE.Utils.API2Request({ + handler: function(btn, e, rec) { + Proxmox.Utils.API2Request({ url: '/api2/extjs/' + baseurl, waitMsgTarget: me, - method: 'PUT', + method: btn.RESTMethod, params: { - 'delete': rec.data.key + 'delete': rec.data.key, }, - callback: function() { - reload(); + callback: () => me.reload(), + failure: response => Ext.Msg.alert('Error', response.htmlStatus), + success: function(response, options) { + if (btn.RESTMethod === 'POST') { + Ext.create('Proxmox.window.TaskProgress', { + autoShow: true, + upid: response.result.data, + listeners: { + destroy: () => me.reload(), + }, + }); + } }, - failure: function (response, opts) { - Ext.Msg.alert('Error', response.htmlStatus); - } }); - } + }, + listeners: { + render: function(btn) { + // hack: calculate the max button width on first display to prevent the whole + // toolbar to move when we switch between the "Remove" and "Detach" labels + var def = btn.getSize().width; + + btn.setText(btn.altText); + var alt = btn.getSize().width; + + btn.setText(btn.defaultText); + + var optimal = alt > def ? alt : def; + btn.setSize({ width: optimal }); + }, + }, }); - var revert_btn = new PVE.button.Button({ - text: gettext('Revert'), - selModel: sm, - disabled: true, - handler: function(b, e, rec) { - var rowdef = me.rows[rec.data.key] || {}; - var keys = rowdef.multiKey || [ rec.data.key ]; - var revert = keys.join(','); - PVE.Utils.API2Request({ - url: '/api2/extjs/' + baseurl, - waitMsgTarget: me, - method: 'PUT', - params: { - 'revert': revert - }, - callback: function() { - reload(); - }, - failure: function (response, opts) { - Ext.Msg.alert('Error',response.htmlStatus); - } - }); - } + let revert_btn = new PVE.button.PendingRevert({ + apiurl: '/api2/extjs/' + baseurl, }); - var efidisk_menuitem = Ext.create('Ext.menu.Item',{ + let efidisk_menuitem = Ext.create('Ext.menu.Item', { text: gettext('EFI Disk'), - iconCls: 'pve-itype-icon-storage', + iconCls: 'fa fa-fw fa-hdd-o black', disabled: !caps.vms['VM.Config.Disk'], handler: function() { + let { data: bios } = me.rstore.getData().map.bios || {}; - var rstoredata = me.rstore.getData().map; - // check if ovmf is configured - if (rstoredata.bios && rstoredata.bios.data.value === 'ovmf') { - var win = Ext.create('PVE.qemu.EFIDiskEdit', { - url: '/api2/extjs/' + baseurl, - pveSelNode: me.pveSelNode - }); - win.on('destroy', reload); - win.show(); - } else { - Ext.Msg.alert('Error',gettext('Please select OVMF(UEFI) as BIOS first.')); - } - - } + Ext.create('PVE.qemu.EFIDiskEdit', { + autoShow: true, + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode, + usesEFI: bios?.value === 'ovmf' || bios?.pending === 'ovmf', + listeners: { + destroy: () => me.reload(), + }, + }); + }, }); - var set_button_status = function() { - var sm = me.getSelectionModel(); - var rec = sm.getSelection()[0]; + let counts = {}; + let isAtLimit = (type) => counts[type] >= PVE.Utils.hardware_counts[type]; + let isAtUsbLimit = () => { + let ostype = me.getObjectValue('ostype'); + let machine = me.getObjectValue('machine'); + return counts.usb >= PVE.Utils.get_max_usb_count(ostype, machine); + }; + + let set_button_status = function() { + let selection_model = me.getSelectionModel(); + let rec = selection_model.getSelection()[0]; + + counts = {}; // en/disable hardwarebuttons + let hasCloudInit = false; + me.rstore.getData().items.forEach(function({ id, data }) { + if (!hasCloudInit && (isCloudInitKey(data.value) || isCloudInitKey(data.pending))) { + hasCloudInit = true; + return; + } + + let match = id.match(/^([^\d]+)\d+$/); + if (match && PVE.Utils.hardware_counts[match[1]] !== undefined) { + let type = match[1]; + counts[type] = (counts[type] || 0) + 1; + } + }); - // disable button when we have an efidisk already - // disable is ok in this case, because you can instantly - // see that there is already one - efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined); + // heuristic only for disabling some stuff, the backend has the final word. + const noSysConsolePerm = !caps.nodes['Sys.Console']; + const noHWPerm = !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use']; + const noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType']; + const noVMConfigNetPerm = !caps.vms['VM.Config.Network']; + const noVMConfigDiskPerm = !caps.vms['VM.Config.Disk']; + const noVMConfigCDROMPerm = !caps.vms['VM.Config.CDROM']; + const noVMConfigCloudinitPerm = !caps.vms['VM.Config.Cloudinit']; + + me.down('#addUsb').setDisabled(noHWPerm || isAtUsbLimit()); + me.down('#addPci').setDisabled(noHWPerm || isAtLimit('hostpci')); + me.down('#addAudio').setDisabled(noVMConfigHWTypePerm || isAtLimit('audio')); + me.down('#addSerial').setDisabled(noVMConfigHWTypePerm || isAtLimit('serial')); + me.down('#addNet').setDisabled(noVMConfigNetPerm || isAtLimit('net')); + me.down('#addRng').setDisabled(noSysConsolePerm || isAtLimit('rng')); + efidisk_menuitem.setDisabled(noVMConfigDiskPerm || isAtLimit('efidisk')); + me.down('#addTpmState').setDisabled(noSysConsolePerm || isAtLimit('tpmstate')); + me.down('#addCloudinitDrive').setDisabled(noVMConfigCDROMPerm || noVMConfigCloudinitPerm || hasCloudInit); if (!rec) { remove_btn.disable(); edit_btn.disable(); - resize_btn.disable(); - move_btn.disable(); - diskthrottle_btn.disable(); + diskaction_btn.disable(); revert_btn.disable(); return; } - var key = rec.data.key; - var value = rec.data.value; - var rowdef = rows[key]; - - var pending = rec.data['delete'] || me.hasPendingChanges(key); - var isDisk = !key.match(/^unused\d+/) && - rowdef.tdCls == 'pve-itype-icon-storage' && - (value && !value.match(/media=cdrom/)); - - var isEfi = (key === 'efidisk0'); - - - remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true)); - - edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor); - - resize_btn.setDisabled(pending || !isDisk); - - move_btn.setDisabled(pending || !isDisk); - - diskthrottle_btn.setDisabled(pending || !isDisk || isEfi); + const { key, value } = rec.data; + const row = rows[key]; + + const deleted = !!rec.data.delete; + const pending = deleted || me.hasPendingChanges(key); + + const isCloudInit = isCloudInitKey(value); + const isCDRom = value && !!value.toString().match(/media=cdrom/); + + const isUnusedDisk = key.match(/^unused\d+/); + const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom; + const isDisk = isUnusedDisk || isUsedDisk; + const isEfi = key === 'efidisk0'; + const tpmMoveable = key === 'tpmstate0' && !me.pveSelNode.data.running; + + let cannotDelete = deleted || row.never_delete; + cannotDelete ||= isCDRom && !cdromCap; + cannotDelete ||= isDisk && !diskCap; + cannotDelete ||= isCloudInit && noVMConfigCloudinitPerm; + remove_btn.setDisabled(cannotDelete); + + remove_btn.setText(isUsedDisk && !isCloudInit ? remove_btn.altText : remove_btn.defaultText); + remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT'; + + edit_btn.setDisabled( + deleted || !row.editor || isCloudInit || (isCDRom && !cdromCap) || (isDisk && !diskCap)); + + diskaction_btn.setDisabled( + pending || + !diskCap || + isCloudInit || + !(isDisk || isEfi || tpmMoveable), + ); + reassign_menuitem.setDisabled(pending || (isEfi || tpmMoveable)); + resize_menuitem.setDisabled(pending || !isUsedDisk); revert_btn.setDisabled(!pending); + }; + let editorFactory = (classPath, extraOptions) => { + extraOptions = extraOptions || {}; + return () => Ext.create(`PVE.qemu.${classPath}`, { + autoShow: true, + url: `/api2/extjs/${baseurl}`, + pveSelNode: me.pveSelNode, + listeners: { + destroy: () => me.reload(), + }, + isAdd: true, + isCreate: true, + ...extraOptions, + }); }; Ext.apply(me, { - url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending', + url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`, interval: 5000, selModel: sm, - tbar: [ + run_editor: run_editor, + tbar: [ { text: gettext('Add'), menu: new Ext.menu.Menu({ + cls: 'pve-add-hw-menu', items: [ { text: gettext('Hard Disk'), - iconCls: 'pve-itype-icon-storage', + iconCls: 'fa fa-fw fa-hdd-o black', disabled: !caps.vms['VM.Config.Disk'], - handler: function() { - var win = Ext.create('PVE.qemu.HDEdit', { - url: '/api2/extjs/' + baseurl, - pveSelNode: me.pveSelNode - }); - win.on('destroy', reload); - win.show(); - } + handler: editorFactory('HDEdit'), }, { text: gettext('CD/DVD Drive'), iconCls: 'pve-itype-icon-cdrom', - disabled: !caps.vms['VM.Config.Disk'], - handler: function() { - var win = Ext.create('PVE.qemu.CDEdit', { - url: '/api2/extjs/' + baseurl, - pveSelNode: me.pveSelNode - }); - win.on('destroy', reload); - win.show(); - } + disabled: !caps.vms['VM.Config.CDROM'], + handler: editorFactory('CDEdit'), }, { text: gettext('Network Device'), - iconCls: 'pve-itype-icon-network', + itemId: 'addNet', + iconCls: 'fa fa-fw fa-exchange black', disabled: !caps.vms['VM.Config.Network'], - handler: function() { - var win = Ext.create('PVE.qemu.NetworkEdit', { - url: '/api2/extjs/' + baseurl, - pveSelNode: me.pveSelNode - }); - win.on('destroy', reload); - win.show(); - } + handler: editorFactory('NetworkEdit'), + }, + efidisk_menuitem, + { + text: gettext('TPM State'), + itemId: 'addTpmState', + iconCls: 'fa fa-fw fa-hdd-o black', + disabled: !caps.vms['VM.Config.Disk'], + handler: editorFactory('TPMDiskEdit'), + }, + { + text: gettext('USB Device'), + itemId: 'addUsb', + iconCls: 'fa fa-fw fa-usb black', + disabled: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], + handler: editorFactory('USBEdit'), + }, + { + text: gettext('PCI Device'), + itemId: 'addPci', + iconCls: 'pve-itype-icon-pci', + disabled: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], + handler: editorFactory('PCIEdit'), + }, + { + text: gettext('Serial Port'), + itemId: 'addSerial', + iconCls: 'pve-itype-icon-serial', + disabled: !caps.vms['VM.Config.Options'], + handler: editorFactory('SerialEdit'), + }, + { + text: gettext('CloudInit Drive'), + itemId: 'addCloudinitDrive', + iconCls: 'fa fa-fw fa-cloud black', + disabled: !caps.vms['VM.Config.CDROM'] || !caps.vms['VM.Config.Cloudinit'], + handler: editorFactory('CIDriveEdit'), + }, + { + text: gettext('Audio Device'), + itemId: 'addAudio', + iconCls: 'fa fa-fw fa-volume-up black', + disabled: !caps.vms['VM.Config.HWType'], + handler: editorFactory('AudioEdit'), }, - efidisk_menuitem - ] - }) + { + text: gettext("VirtIO RNG"), + itemId: 'addRng', + iconCls: 'pve-itype-icon-die', + disabled: !caps.nodes['Sys.Console'], + handler: editorFactory('RNGEdit'), + }, + ], + }), }, remove_btn, edit_btn, - resize_btn, - move_btn, - diskthrottle_btn, - cpuoptions_btn, - revert_btn + diskaction_btn, + revert_btn, ], rows: rows, sorterFn: sorterFn, listeners: { itemdblclick: run_editor, - selectionchange: set_button_status - } + selectionchange: set_button_status, + }, }); me.callParent(); - me.on('activate', me.rstore.startUpdate); - me.on('destroy', me.rstore.stopUpdate); + me.on('activate', me.rstore.startUpdate, me.rstore); + me.on('destroy', me.rstore.stopUpdate, me.rstore); - me.mon(me.rstore, 'refresh', function() { - set_button_status(); - }); - } + me.mon(me.getStore(), 'datachanged', set_button_status, me); + }, });