mirror of
https://git.proxmox.com/git/pve-manager
synced 2025-06-17 15:51:35 +00:00

in the backend, we allow up to 14 usb ports, but only if the vm can use the qemu-xhci controller which is only possible since machine version 7.1 and if the ostype is l26 or windows > 7 for this we introduce two helpers: * qemu_min_version: modeled after the signature of 'min_version' from qemu-server, expects two arrays of versions and returns true if the first parameter is equal or greater than the second version * get_max_usb_count looks at the given ostype and machine string and returns the proper maximum number since we don't currently have the actual running version of the vm in the gui, this is only a heuristic for running vms. but since the actual running version could only be lower if none is set (e.g. for migrated/long-running vms) we allow more in the gui and the backend will do the proper thing (either hotplug it, or make it a pending change) Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
750 lines
20 KiB
JavaScript
750 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/);
|
|
|
|
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-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);
|
|
},
|
|
});
|