pve-manager/www/manager6/qemu/HardwareView.js
Stefan Reiter 4a0ccbadc3 ui/hardware: extend machine renderer to show pinned Windows versions
The default is not just "i440fx", this hides the fact that the version
will be pinned to 5.1, unless one deliberately opens the editor.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-03-08 11:05:13 +01:00

780 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'] ? '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'),
};
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);
},
});