5c63f8a1fe
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
762 lines
21 KiB
JavaScript
762 lines
21 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'];
|
|
const noVMConfigCDROMPerm = !caps.vms['VM.Config.CDROM'];
|
|
const noVMConfigCloudinitPerm = !caps.vms['VM.Config.Cloudinit'];
|
|
|
|
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(noVMConfigCDROMPerm || noVMConfigCloudinitPerm || 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/);
|
|
|
|
const isUnusedDisk = key.match(/^unused\d+/);
|
|
const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom;
|
|
const isDisk = isUnusedDisk || isUsedDisk;
|
|
const isEfi = key === 'efidisk0';
|
|
const tpmMoveable = key === 'tpmstate0' && !me.pveSelNode.data.running;
|
|
|
|
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 ? '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.vms['VM.Config.CDROM'] || !caps.vms['VM.Config.Cloudinit'],
|
|
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);
|
|
},
|
|
});
|