pve-manager/www/manager6/qemu/HardwareView.js
Aaron Lauterer a1e25c47f3 ui: qemu/HardwareView: fix CDRom permission checkss
There are several types of drives that use the same config keys. Most
notably CDRom and regular VM disks (EFI and cloudinit exist as well).

Since there is a dedicated permission for CDRom drives we need to check
permissions in more detail, depending on what type of drive it actually
is for things like the edit, remove and Add -> CDRom buttons.

The permission check in the row definition itself which only checked for
'VM.Config.Disk' permissions (never_delete) had to be removed and finer
grained checks added for the individual buttons. This also meant a bit
of reshuffling in the checks what kind of disk the current one is.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-02-19 17:22:30 +01:00

784 lines
20 KiB
JavaScript

Ext.define('PVE.qemu.HardwareView', {
extend: 'Proxmox.grid.PendingObjectGrid',
alias: ['widget.PVE.qemu.HardwareView'],
onlineHelp: 'qm_virtual_machines_settings',
renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
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;
} else if (iconCls) {
icon = "<i class='pve-grid-fa fa fa-fw fa-" + iconCls + "'></i>";
metaData.tdCls += " pve-itype-fa";
}
// only return icons in grid but not remove dialog
if (rowIndex !== undefined) {
return icon + txt;
} else {
return txt;
}
},
initComponent: function() {
var me = this;
const nodename = me.pveSelNode.data.node;
if (!nodename) {
throw "no node name specified";
}
const vmid = me.pveSelNode.data.vmid;
if (!vmid) {
throw "no VM ID specified";
}
const caps = Ext.state.Manager.get('GuiCap');
const diskCap = caps.vms['VM.Config.Disk'];
const cdromCap = caps.vms['VM.Config.CDROM'];
let rows = {
memory: {
header: gettext('Memory'),
editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
never_delete: true,
defaultValue: '512',
tdCls: 'pve-itype-icon-memory',
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',
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);
var numa = me.getObjectValue('numa', undefined, pending);
var vcpus = me.getObjectValue('vcpus', undefined, pending);
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);
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',
group: 4,
never_delete: true,
editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
defaultValue: '',
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,
iconCls: 'desktop',
group: 5,
defaultValue: '',
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,
iconCls: 'cogs',
never_delete: true,
group: 6,
defaultValue: '',
renderer: PVE.Utils.render_qemu_machine,
},
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,
},
cpu: {
visible: false,
},
numa: {
visible: false,
},
balloon: {
visible: false,
},
hotplug: {
visible: false,
},
vcpus: {
visible: false,
},
cpuunits: {
visible: false,
},
cpulimit: {
visible: false,
},
shares: {
visible: false,
},
};
PVE.Utils.forEachBus(undefined, function(type, id) {
let confid = type + id;
rows[confid] = {
group: 10,
iconCls: 'hdd-o',
editor: 'PVE.qemu.HDEdit',
isOnStorageBus: true,
header: gettext('Hard Disk') + ' (' + confid +')',
cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')',
};
});
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'],
header: gettext('Network Device') + ' (' + confid +')',
};
}
rows.efidisk0 = {
group: 20,
iconCls: 'hdd-o',
editor: null,
never_delete: !caps.vms['VM.Config.Disk'],
header: gettext('EFI Disk'),
};
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'],
header: gettext('USB Device') + ' (' + confid + ')',
};
}
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'],
editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined,
header: gettext('PCI Device') + ' (' + confid + ')',
};
}
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'],
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'],
header: gettext('Audio Device'),
};
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(),
};
}
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;
var order1 = rows[v1].order || 0;
var order2 = rows[v2].order || 0;
if (g1 - g2 !== 0) {
return g1 - g2;
}
if (order1 - order2 !== 0) {
return order1 - order2;
}
if (v1 > v2) {
return 1;
} else if (v1 < v2) {
return -1;
} else {
return 0;
}
};
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;
}
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/)) {
return;
} else if (value.match(/media=cdrom/)) {
editor = 'PVE.qemu.CDEdit';
} else if (!diskCap) {
return;
}
}
var win;
if (Ext.isString(editor)) {
win = Ext.create(editor, {
pveSelNode: me.pveSelNode,
confid: rec.data.key,
url: '/api2/extjs/' + baseurl,
});
} 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();
}
win.show();
win.on('destroy', me.reload, me);
};
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', me.reload, me);
};
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', me.reload, me);
};
var edit_btn = new Proxmox.button.Button({
text: gettext('Edit'),
selModel: sm,
disabled: true,
handler: run_editor,
});
var resize_btn = new Proxmox.button.Button({
text: gettext('Resize disk'),
selModel: sm,
disabled: true,
handler: run_resize,
});
var move_btn = new Proxmox.button.Button({
text: gettext('Move disk'),
selModel: sm,
disabled: true,
handler: run_move,
});
var 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 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}');
}
var key = rec.data.key;
var entry = rows[key];
var rendered = me.renderKey(key, {}, rec);
var msg = Ext.String.format(warn, "'" + rendered + "'");
if (entry.del_extra_msg) {
msg += '<br>' + entry.del_extra_msg;
}
return msg;
},
handler: function(b, e, rec) {
Proxmox.Utils.API2Request({
url: '/api2/extjs/' + baseurl,
waitMsgTarget: me,
method: b.RESTMethod,
params: {
'delete': rec.data.key,
},
callback: () => me.reload(),
failure: function(response, opts) {
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,
listeners: {
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
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.PendingRevert({
apiurl: '/api2/extjs/' + baseurl,
});
var 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');
var win = Ext.create('PVE.qemu.EFIDiskEdit', {
url: '/api2/extjs/' + baseurl,
pveSelNode: me.pveSelNode,
usesEFI: usesEFI,
});
win.on('destroy', me.reload, me);
win.show();
},
});
let counts = {};
let isAtLimit = (type) => counts[type] >= PVE.Utils.hardware_counts[type];
var set_button_status = function() {
var selection_model = me.getSelectionModel();
var rec = selection_model.getSelection()[0];
// en/disable hardwarebuttons
counts = {};
var hasCloudInit = false;
me.rstore.getData().items.forEach(function(item) {
if (!hasCloudInit && (
/vm-.*-cloudinit/.test(item.data.value) ||
/vm-.*-cloudinit/.test(item.data.pending)
)) {
hasCloudInit = true;
return;
}
let match = item.id.match(/^([^\d]+)\d+$/);
let type;
if (match && PVE.Utils.hardware_counts[match[1]] !== undefined) {
type = match[1];
} else {
return;
}
counts[type] = (counts[type] || 0) + 1;
});
// heuristic only for disabling some stuff, the backend has the final word.
const noSysConsolePerm = !caps.nodes['Sys.Console'];
const noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType'];
const noVMConfigNetPerm = !caps.vms['VM.Config.Network'];
const noVMConfigDiskPerm = !caps.vms['VM.Config.Disk'];
me.down('#addusb').setDisabled(noSysConsolePerm || isAtLimit('usb'));
me.down('#addpci').setDisabled(noSysConsolePerm || 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('#addci').setDisabled(noSysConsolePerm || hasCloudInit);
if (!rec) {
remove_btn.disable();
edit_btn.disable();
resize_btn.disable();
move_btn.disable();
revert_btn.disable();
return;
}
const key = rec.data.key;
const value = rec.data.value;
const row = rows[key];
const deleted = !!rec.data.delete;
const pending = deleted || me.hasPendingChanges(key);
const isCloudInit = value && value.toString().match(/vm-.*-cloudinit/);
const isCDRom = value && !!value.toString().match(/media=cdrom/) && !isCloudInit;
const isUnusedDisk = key.match(/^unused\d+/);
const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom && !isCloudInit;
const isDisk = isCloudInit || isUnusedDisk || isUsedDisk;
const isEfi = key === 'efidisk0';
remove_btn.setDisabled(
deleted ||
row.never_delete ||
(isCDRom && !cdromCap) ||
(isDisk && !diskCap),
);
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),
);
resize_btn.setDisabled(pending || !isUsedDisk || !diskCap);
move_btn.setDisabled(pending || !(isUsedDisk || isEfi) || !diskCap);
revert_btn.setDisabled(!pending);
};
Ext.apply(me, {
url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`,
interval: 5000,
selModel: sm,
run_editor: run_editor,
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() {
let win = Ext.create('PVE.qemu.HDEdit', {
url: '/api2/extjs/' + baseurl,
pveSelNode: me.pveSelNode,
});
win.on('destroy', me.reload, me);
win.show();
},
},
{
text: gettext('CD/DVD Drive'),
iconCls: 'pve-itype-icon-cdrom',
disabled: !caps.vms['VM.Config.CDROM'],
handler: function() {
let win = Ext.create('PVE.qemu.CDEdit', {
url: '/api2/extjs/' + baseurl,
pveSelNode: me.pveSelNode,
});
win.on('destroy', me.reload, me);
win.show();
},
},
{
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', me.reload, me);
win.show();
},
},
efidisk_menuitem,
{
text: gettext('USB Device'),
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', me.reload, me);
win.show();
},
},
{
text: gettext('PCI Device'),
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', me.reload, me);
win.show();
},
},
{
text: gettext('Serial Port'),
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', me.reload, me);
win.show();
},
},
{
text: gettext('CloudInit Drive'),
itemId: 'addci',
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', me.reload, me);
win.show();
},
},
{
text: gettext('Audio Device'),
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', me.reload, me);
win.show();
},
},
{
text: gettext("VirtIO RNG"),
itemId: 'addrng',
iconCls: 'pve-itype-icon-die',
disabled: !caps.nodes['Sys.Console'],
handler: function() {
var win = Ext.create('PVE.qemu.RNGEdit', {
url: '/api2/extjs/' + baseurl,
isCreate: true,
isAdd: true,
});
win.on('destroy', me.reload, me);
win.show();
},
},
],
}),
},
remove_btn,
edit_btn,
resize_btn,
move_btn,
revert_btn,
],
rows: rows,
sorterFn: sorterFn,
listeners: {
itemdblclick: run_editor,
selectionchange: set_button_status,
},
});
me.callParent();
me.on('activate', me.rstore.startUpdate, me.rstore);
me.on('destroy', me.rstore.stopUpdate, me.rstore);
me.mon(me.getStore(), 'datachanged', set_button_status, me);
},
});