X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=www%2Fmanager6%2Fqemu%2FHardwareView.js;h=af35a980f88d4e8a9f211912af3eee392538ce25;hb=refs%2Fheads%2Fmaster;hp=29cdb68ce4598692840938b542889f1cf26dc12e;hpb=141aba6b62b9e80845b1abaa8fa10c3b90f90326;p=pve-manager.git diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js index 29cdb68c..e1902695 100644 --- a/www/manager6/qemu/HardwareView.js +++ b/www/manager6/qemu/HardwareView.js @@ -10,7 +10,7 @@ Ext.define('PVE.qemu.HardwareView', { var rowdef = rows[key] || {}; var iconCls = rowdef.iconCls; var icon = ''; - var txt = (rowdef.header || key); + var txt = rowdef.header || key; metaData.tdAttr = "valign=middle"; @@ -43,25 +43,29 @@ Ext.define('PVE.qemu.HardwareView', { } }, - 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'); - var diskCap = caps.vms['VM.Config.Disk']; + 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/); + + const nodeInfo = PVE.data.ResourceStore.getNodes().find(node => node.node === nodename); + let processorEditor = { + xtype: 'pveQemuProcessorEdit', + cgroupMode: nodeInfo['cgroup-mode'], + }; - /*jslint confusion: true */ - var rows = { + let rows = { memory: { header: gettext('Memory'), editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined, @@ -74,10 +78,10 @@ Ext.define('PVE.qemu.HardwareView', { var res = ''; var max = me.getObjectValue('memory', 512, pending); - var balloon = me.getObjectValue('balloon', undefined, pending); + var balloon = me.getObjectValue('balloon', undefined, pending); var shares = me.getObjectValue('shares', undefined, pending); - res = Proxmox.Utils.format_size(max*1024*1024); + res = Proxmox.Utils.format_size(max*1024*1024); if (balloon !== undefined && balloon > 0) { res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res; @@ -89,19 +93,18 @@ Ext.define('PVE.qemu.HardwareView', { 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', + 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); @@ -110,31 +113,27 @@ Ext.define('PVE.qemu.HardwareView', { var cpulimit = me.getObjectValue('cpulimit', undefined, pending); var cpuunits = me.getObjectValue('cpuunits', undefined, pending); - var res = Ext.String.format('{0} ({1} sockets, {2} cores)', - sockets*cores, sockets, 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; - } + }, }, bios: { header: 'BIOS', @@ -143,38 +142,32 @@ Ext.define('PVE.qemu.HardwareView', { editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined, defaultValue: '', iconCls: 'microchip', - renderer: PVE.Utils.render_qemu_bios + renderer: PVE.Utils.render_qemu_bios, }, vga: { header: gettext('Display'), editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined, never_delete: true, iconCls: 'desktop', - group:5, + group: 5, defaultValue: '', - renderer: PVE.Utils.render_kvm_vga_driver + renderer: PVE.Utils.render_kvm_vga_driver, }, machine: { header: gettext('Machine'), - editor: caps.vms['VM.Config.HWType'] ? { - xtype: 'proxmoxWindowEdit', - subject: gettext('Machine'), - width: 350, - items: [{ - xtype: 'proxmoxKVComboBox', - name: 'machine', - value: '__default__', - fieldLabel: gettext('Machine'), - comboItems: [ - ['__default__', PVE.Utils.render_qemu_machine('')], - ['q35', 'q35'] - ] - }]} : undefined, + editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.MachineEdit' : undefined, iconCls: 'cogs', never_delete: true, group: 6, defaultValue: '', - renderer: PVE.Utils.render_qemu_machine + 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'), @@ -183,117 +176,139 @@ Ext.define('PVE.qemu.HardwareView', { renderer: PVE.Utils.render_scsihw, group: 7, never_delete: true, - defaultValue: '' + 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 - } + visible: false, + }, + ostype: { + visible: false, + }, }; - /*jslint confusion: false */ PVE.Utils.forEachBus(undefined, function(type, id) { - var confid = type + id; + let confid = type + id; rows[confid] = { 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 +')', - cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')' + cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')', }; }); - for (i = 0; i < 32; i++) { - confid = "net" + i.toString(); + for (let i = 0; i < PVE.Utils.hardware_counts.net; i++) { + let confid = "net" + i.toString(); rows[confid] = { group: 15, order: i, iconCls: 'exchange', editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined, - never_delete: caps.vms['VM.Config.Network'] ? false : true, - header: gettext('Network Device') + ' (' + confid +')' + never_delete: !caps.vms['VM.Config.Network'], + header: gettext('Network Device') + ' (' + confid +')', }; } rows.efidisk0 = { group: 20, iconCls: 'hdd-o', editor: null, - never_delete: caps.vms['VM.Config.Disk'] ? false : true, - header: gettext('EFI Disk') + never_delete: !caps.vms['VM.Config.Disk'], + header: gettext('EFI Disk'), }; - for (i = 0; i < 5; i++) { - confid = "usb" + i.toString(); + 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: 25, order: i, iconCls: 'usb', - editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined, - never_delete: caps.nodes['Sys.Console'] ? false : true, - header: gettext('USB Device') + ' (' + confid + ')' + 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 < 4; i++) { - confid = "hostpci" + i.toString(); + for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) { + let confid = "hostpci" + i.toString(); rows[confid] = { group: 30, order: i, tdCls: 'pve-itype-icon-pci', - never_delete: caps.nodes['Sys.Console'] ? false : true, - editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined, - header: gettext('PCI Device') + ' (' + confid + ')' + 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 < 4; i++) { - confid = "serial" + i.toString(); + for (let i = 0; i < PVE.Utils.hardware_counts.serial; i++) { + let confid = "serial" + i.toString(); rows[confid] = { group: 35, order: i, tdCls: 'pve-itype-icon-serial', - never_delete: caps.nodes['Sys.Console'] ? false : true, - header: gettext('Serial Port') + ' (' + confid + ')' + never_delete: !caps.nodes['Sys.Console'], + header: gettext('Serial Port') + ' (' + confid + ')', }; } rows.audio0 = { group: 40, iconCls: 'volume-up', editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined, - never_delete: caps.vms['VM.Config.HWType'] ? false : true, - header: gettext('Audio Device') + never_delete: !caps.vms['VM.Config.HWType'], + header: gettext('Audio Device'), }; - for (i = 0; i < 256; i++) { + 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.toString() + 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; @@ -303,11 +318,11 @@ Ext.define('PVE.qemu.HardwareView', { var order1 = rows[v1].order || 0; var order2 = rows[v2].order || 0; - if ((g1 - g2) !== 0) { + if (g1 - g2 !== 0) { return g1 - g2; } - - if ((order1 - order2) !== 0) { + + if (order1 - order2 !== 0) { return order1 - order2; } @@ -320,29 +335,21 @@ Ext.define('PVE.qemu.HardwareView', { } }; - var reload = function() { - me.rstore.load(); - }; - - var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config'; + let baseurl = `nodes/${nodename}/qemu/${vmid}/config`; - var sm = Ext.create('Ext.selection.RowModel', {}); + let sm = Ext.create('Ext.selection.RowModel', {}); - var run_editor = 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; - var rowdef = rows[rec.data.key]; - if (!rowdef.editor) { - return; - } - - var editor = rowdef.editor; if (rowdef.isOnStorageBus) { - var value = me.getObjectValue(rec.data.key, '', true); - if (value.match(/vm-.*-cloudinit/)) { + let value = me.getObjectValue(rec.data.key, '', true); + if (isCloudInitKey(value)) { return; } else if (value.match(/media=cdrom/)) { editor = 'PVE.qemu.CDEdit'; @@ -351,84 +358,113 @@ Ext.define('PVE.qemu.HardwareView', { } } - var win; + let commonOpts = { + autoShow: true, + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: `/api2/extjs/${baseurl}`, + listeners: { + destroy: () => me.reload(), + }, + }; if (Ext.isString(editor)) { - win = Ext.create(editor, { - pveSelNode: me.pveSelNode, - confid: rec.data.key, - url: '/api2/extjs/' + baseurl - }); + Ext.create(editor, commonOpts); } 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); + let win = Ext.createWidget(rowdef.editor.xtype, Ext.apply(commonOpts, rowdef.editor)); win.load(); } - - win.show(); - win.on('destroy', reload); - }; - - var run_resize = function() { - var rec = sm.getSelection()[0]; - if (!rec) { - return; - } - - var win = Ext.create('PVE.window.HDResize', { - disk: rec.data.key, - nodename: nodename, - vmid: vmid - }); - - win.show(); - - win.on('destroy', reload); }; - var run_move = function() { - var rec = sm.getSelection()[0]; - if (!rec) { - return; - } - - 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 Proxmox.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 Proxmox.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 Proxmox.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, + 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(), + }, + }); + }, + }); + + let resize_menuitem = new Ext.menu.Item({ + text: gettext('Resize'), + iconCls: 'fa fa-plus', + selModel: sm, + 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(), + }, + }); + }, + }); + + let diskaction_btn = new Proxmox.button.Button({ + text: gettext('Disk Action'), disabled: true, - handler: run_move + menu: { + items: [ + move_menuitem, + reassign_menuitem, + resize_menuitem, + ], + }, }); - var remove_btn = new Proxmox.button.Button({ + + let remove_btn = new Proxmox.button.Button({ text: gettext('Remove'), defaultText: gettext('Remove'), altText: gettext('Detach'), @@ -437,56 +473,47 @@ Ext.define('PVE.qemu.HardwareView', { dangerous: true, RESTMethod: 'PUT', confirmMsg: function(rec) { - var warn = gettext('Are you sure you want to remove entry {0}'); + 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}'`); - var entry = rec.data.key; - var rendered = me.renderKey(entry, {}, rec); - var msg = Ext.String.format(warn, "'" + rendered + "'"); - - if (entry.match(/^unused\d+$/)) { - msg += " " + gettext('This will permanently erase all data.'); + if (rows[rec.data.key].del_extra_msg) { + msg += '
' + rows[rec.data.key].del_extra_msg; } - return msg; }, - handler: function(b, e, rec) { + handler: function(btn, e, rec) { + let params = { 'delete': rec.data.key }; + if (btn.RESTMethod === 'POST') { + params.background_delay = 5; + } Proxmox.Utils.API2Request({ url: '/api2/extjs/' + baseurl, waitMsgTarget: me, - method: b.RESTMethod, - params: { - 'delete': rec.data.key - }, - callback: function() { - reload(); - }, - failure: function (response, opts) { - Ext.Msg.alert('Error', response.htmlStatus); - }, + method: btn.RESTMethod, + params: params, + callback: () => me.reload(), + failure: response => Ext.Msg.alert('Error', response.htmlStatus), success: function(response, options) { - if (b.RESTMethod === 'POST') { - var upid = response.result.data; - var win = Ext.create('Proxmox.window.TaskProgress', { - upid: upid, + if (btn.RESTMethod === 'POST' && response.result.data !== null) { + Ext.create('Proxmox.window.TaskProgress', { + autoShow: true, + upid: response.result.data, listeners: { - destroy: function () { - me.reload(); - } - } + destroy: () => me.reload(), + }, }); - win.show(); } - } + }, }); }, listeners: { render: function(btn) { - // hack: calculate an optimal button width on first display - // to prevent the whole toolbar to move when we switch - // between the "Remove" and "Detach" labels + // 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); @@ -496,268 +523,242 @@ Ext.define('PVE.qemu.HardwareView', { var optimal = alt > def ? alt : def; btn.setSize({ width: optimal }); - } - } + }, + }, }); - var revert_btn = new Proxmox.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(','); - Proxmox.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: 'fa fa-fw fa-hdd-o black', disabled: !caps.vms['VM.Config.Disk'], handler: function() { - let bios = me.rstore.getData().map.bios; - let usesEFI = bios && (bios.data.value === 'ovmf' || bios.data.pending === 'ovmf'); + let { data: bios } = me.rstore.getData().map.bios || {}; - var win = Ext.create('PVE.qemu.EFIDiskEdit', { + Ext.create('PVE.qemu.EFIDiskEdit', { + autoShow: true, url: '/api2/extjs/' + baseurl, pveSelNode: me.pveSelNode, - usesEFI: usesEFI, + usesEFI: bios?.value === 'ovmf' || bios?.pending === 'ovmf', + listeners: { + destroy: () => me.reload(), + }, }); - win.on('destroy', reload); - win.show(); - } + }, }); - var set_button_status = function() { - var sm = me.getSelectionModel(); - var rec = sm.getSelection()[0]; - - // 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); - // en/disable usb add button - var usbcount = 0; - var pcicount = 0; - var audiocount = 0; - var hasCloudInit = false; - me.rstore.getData().items.forEach(function(item){ - if (/^usb\d+/.test(item.id)) { - usbcount++; - } else if (/^hostpci\d+/.test(item.id)) { - pcicount++; - } else if (/^audio\d+/.test(item.id)) { - audiocount++; - } - if (!hasCloudInit && /vm-.*-cloudinit/.test(item.data.value)) { + 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; } }); // heuristic only for disabling some stuff, the backend has the final word. - var noSysConsolePerm = !caps.nodes['Sys.Console']; - var noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType']; - - me.down('#addusb').setDisabled(noSysConsolePerm || (usbcount >= 5)); - me.down('#addpci').setDisabled(noSysConsolePerm || (pcicount >= 4)); - me.down('#addaudio').setDisabled(noVMConfigHWTypePerm || (audiocount >= 1)); - me.down('#addci').setDisabled(noSysConsolePerm || hasCloudInit); + 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(); + 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 isCDRom = (value && !!value.toString().match(/media=cdrom/)); - var isUnusedDisk = key.match(/^unused\d+/); - var isUsedDisk = !isUnusedDisk && rowdef.isOnStorageBus && !isCDRom; - - var isCloudInit = (value && value.toString().match(/vm-.*-cloudinit/)); - - var isEfi = (key === 'efidisk0'); - - remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true) || (isUnusedDisk && !diskCap)); - remove_btn.setText((isUsedDisk && !isCloudInit) ? remove_btn.altText : remove_btn.defaultText); - remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT'; - - edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor || isCloudInit || (!isCDRom && !diskCap)); - - resize_btn.setDisabled(pending || !isUsedDisk || !diskCap); - - move_btn.setDisabled(pending || !(isUsedDisk || isEfi) || !diskCap); + const { key, value } = rec.data; + const row = rows[key]; + + const deleted = !!rec.data.delete; + const pending = deleted || me.hasPendingChanges(key); + const isRunning = me.pveSelNode.data.running; + + 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' && !isRunning; + + 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 || (isDisk && isRunning) ? '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, run_editor: run_editor, - tbar: [ + tbar: [ { text: gettext('Add'), menu: new Ext.menu.Menu({ + cls: 'pve-add-hw-menu', items: [ { text: gettext('Hard Disk'), 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'), + 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, - isCreate: true - }); - 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', + itemId: 'addUsb', iconCls: 'fa fa-fw fa-usb black', - disabled: !caps.nodes['Sys.Console'], - handler: function() { - var win = Ext.create('PVE.qemu.USBEdit', { - url: '/api2/extjs/' + baseurl, - pveSelNode: me.pveSelNode - }); - win.on('destroy', reload); - win.show(); - } + disabled: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], + handler: editorFactory('USBEdit'), }, { text: gettext('PCI Device'), - itemId: 'addpci', + itemId: 'addPci', iconCls: 'pve-itype-icon-pci', - disabled: !caps.nodes['Sys.Console'], - handler: function() { - var win = Ext.create('PVE.qemu.PCIEdit', { - url: '/api2/extjs/' + baseurl, - pveSelNode: me.pveSelNode - }); - win.on('destroy', reload); - win.show(); - } + disabled: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], + handler: editorFactory('PCIEdit'), }, { text: gettext('Serial Port'), - itemId: 'addserial', + itemId: 'addSerial', iconCls: 'pve-itype-icon-serial', disabled: !caps.vms['VM.Config.Options'], - handler: function() { - var win = Ext.create('PVE.qemu.SerialEdit', { - url: '/api2/extjs/' + baseurl - }); - win.on('destroy', reload); - win.show(); - } + handler: editorFactory('SerialEdit'), }, { text: gettext('CloudInit Drive'), - itemId: 'addci', + itemId: 'addCloudinitDrive', iconCls: 'fa fa-fw fa-cloud black', - disabled: !caps.nodes['Sys.Console'], - handler: function() { - var win = Ext.create('PVE.qemu.CIDriveEdit', { - url: '/api2/extjs/' + baseurl, - pveSelNode: me.pveSelNode - }); - win.on('destroy', reload); - win.show(); - } + disabled: !caps.vms['VM.Config.CDROM'] || !caps.vms['VM.Config.Cloudinit'], + handler: editorFactory('CIDriveEdit'), }, { text: gettext('Audio Device'), - itemId: 'addaudio', + itemId: 'addAudio', iconCls: 'fa fa-fw fa-volume-up black', disabled: !caps.vms['VM.Config.HWType'], - handler: function() { - var win = Ext.create('PVE.qemu.AudioEdit', { - url: '/api2/extjs/' + baseurl, - isCreate: true, - isAdd: true - }); - win.on('destroy', reload); - win.show(); - } - } - ] - }) + handler: editorFactory('AudioEdit'), + }, + { + 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, - 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.getStore(), 'datachanged', function() { - set_button_status(); - }); - } + me.mon(me.getStore(), 'datachanged', set_button_status, me); + }, });