pve-manager/www/manager6/qemu/HardwareView.js
Fiona Ebner 5a2e333c9e ui: lxc/qemu: cpu edit: make cpuunits depend on node's cgroup version
so that the default value and limits actually correspond to what will
be used. Defaults to values for cgroup v2, because that is the more
common scenario.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
2022-11-15 14:08:55 +01:00

756 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 { node: nodename, vmid } = me.pveSelNode.data;
if (!nodename) {
throw "no node name specified";
} else 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 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'],
};
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']
? 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);
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);
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',
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'] ? 'PVE.qemu.MachineEdit' : undefined,
iconCls: 'cogs',
never_delete: true,
group: 6,
defaultValue: '',
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,
},
cpu: {
visible: false,
},
numa: {
visible: false,
},
balloon: {
visible: false,
},
hotplug: {
visible: false,
},
vcpus: {
visible: false,
},
cpuunits: {
visible: false,
},
cpulimit: {
visible: false,
},
shares: {
visible: false,
},
ostype: {
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'),
};
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'],
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;
}
};
let baseurl = `nodes/${nodename}/qemu/${vmid}/config`;
let sm = Ext.create('Ext.selection.RowModel', {});
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;
}
}
let commonOpts = {
autoShow: true,
pveSelNode: me.pveSelNode,
confid: rec.data.key,
url: `/api2/extjs/${baseurl}`,
listeners: {
destroy: () => me.reload(),
},
};
if (Ext.isString(editor)) {
Ext.create(editor, commonOpts);
} else {
let win = Ext.createWidget(rowdef.editor.xtype, Ext.apply(commonOpts, rowdef.editor));
win.load();
}
};
let edit_btn = new Proxmox.button.Button({
text: gettext('Edit'),
selModel: sm,
disabled: true,
handler: run_editor,
});
let move_menuitem = new Ext.menu.Item({
text: gettext('Move Storage'),
tooltip: gettext('Move disk to another storage'),
iconCls: 'fa fa-database',
selModel: sm,
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(),
},
});
},
});
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,
menu: {
items: [
move_menuitem,
reassign_menuitem,
resize_menuitem,
],
},
});
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) {
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 += '<br>' + rows[rec.data.key].del_extra_msg;
}
return msg;
},
handler: function(btn, e, rec) {
Proxmox.Utils.API2Request({
url: '/api2/extjs/' + baseurl,
waitMsgTarget: me,
method: btn.RESTMethod,
params: {
'delete': rec.data.key,
},
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(),
},
});
}
},
});
},
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 });
},
},
});
let revert_btn = new PVE.button.PendingRevert({
apiurl: '/api2/extjs/' + baseurl,
});
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 { data: bios } = me.rstore.getData().map.bios || {};
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(),
},
});
},
});
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.
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 || isAtUsbLimit());
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('#addTpmState').setDisabled(noSysConsolePerm || isAtLimit('tpmstate'));
me.down('#addCloudinitDrive').setDisabled(noSysConsolePerm || hasCloudInit);
if (!rec) {
remove_btn.disable();
edit_btn.disable();
diskaction_btn.disable();
revert_btn.disable();
return;
}
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/) && !isCloudInit;
const isUnusedDisk = key.match(/^unused\d+/);
const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom && !isCloudInit;
const isDisk = isCloudInit || isUnusedDisk || isUsedDisk;
const isEfi = key === 'efidisk0';
const tpmMoveable = key === 'tpmstate0' && !me.pveSelNode.data.running;
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));
diskaction_btn.setDisabled(
pending ||
!diskCap ||
isCloudInit ||
!(isDisk || isEfi || tpmMoveable),
);
move_menuitem.setDisabled(isUnusedDisk);
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`,
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: editorFactory('HDEdit'),
},
{
text: gettext('CD/DVD Drive'),
iconCls: 'pve-itype-icon-cdrom',
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: 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'],
handler: editorFactory('USBEdit'),
},
{
text: gettext('PCI Device'),
itemId: 'addPci',
iconCls: 'pve-itype-icon-pci',
disabled: !caps.nodes['Sys.Console'],
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.nodes['Sys.Console'],
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'),
},
{
text: gettext("VirtIO RNG"),
itemId: 'addRng',
iconCls: 'pve-itype-icon-die',
disabled: !caps.nodes['Sys.Console'],
handler: editorFactory('RNGEdit'),
},
],
}),
},
remove_btn,
edit_btn,
diskaction_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);
},
});