]>
git.proxmox.com Git - pve-manager.git/blob - www/manager6/qemu/HardwareView.js
1 Ext
.define('PVE.qemu.HardwareView', {
2 extend
: 'Proxmox.grid.PendingObjectGrid',
3 alias
: ['widget.PVE.qemu.HardwareView'],
5 onlineHelp
: 'qm_virtual_machines_settings',
7 renderKey: function(key
, metaData
, rec
, rowIndex
, colIndex
, store
) {
10 var rowdef
= rows
[key
] || {};
11 var iconCls
= rowdef
.iconCls
;
13 var txt
= rowdef
.header
|| key
;
15 metaData
.tdAttr
= "valign=middle";
17 if (rowdef
.isOnStorageBus
) {
18 var value
= me
.getObjectValue(key
, '', false);
20 value
= me
.getObjectValue(key
, '', true);
22 if (value
.match(/vm-.*-cloudinit/)) {
24 txt
= rowdef
.cloudheader
;
25 } else if (value
.match(/media=cdrom/)) {
26 metaData
.tdCls
= 'pve-itype-icon-cdrom';
27 return rowdef
.cdheader
;
32 metaData
.tdCls
= rowdef
.tdCls
;
34 icon
= "<i class='pve-grid-fa fa fa-fw fa-" + iconCls
+ "'></i>";
35 metaData
.tdCls
+= " pve-itype-fa";
38 // only return icons in grid but not remove dialog
39 if (rowIndex
!== undefined) {
46 initComponent: function() {
49 const { node
: nodename
, vmid
} = me
.pveSelNode
.data
;
51 throw "no node name specified";
53 throw "no VM ID specified";
56 const caps
= Ext
.state
.Manager
.get('GuiCap');
57 const diskCap
= caps
.vms
['VM.Config.Disk'];
58 const cdromCap
= caps
.vms
['VM.Config.CDROM'];
60 let isCloudInitKey
= v
=> v
&& v
.toString().match(/vm-.*-cloudinit/);
62 const nodeInfo
= PVE
.data
.ResourceStore
.getNodes().find(node
=> node
.node
=== nodename
);
63 let processorEditor
= {
64 xtype
: 'pveQemuProcessorEdit',
65 cgroupMode
: nodeInfo
['cgroup-mode'],
70 header
: gettext('Memory'),
71 editor
: caps
.vms
['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
74 tdCls
: 'pve-itype-icon-memory',
76 multiKey
: ['memory', 'balloon', 'shares'],
77 renderer: function(value
, metaData
, record
, ri
, ci
, store
, pending
) {
80 var max
= me
.getObjectValue('memory', 512, pending
);
81 var balloon
= me
.getObjectValue('balloon', undefined, pending
);
82 var shares
= me
.getObjectValue('shares', undefined, pending
);
84 res
= Proxmox
.Utils
.format_size(max
*1024*1024);
86 if (balloon
!== undefined && balloon
> 0) {
87 res
= Proxmox
.Utils
.format_size(balloon
*1024*1024) + "/" + res
;
90 res
+= ' [shares=' + shares
+']';
92 } else if (balloon
=== 0) {
93 res
+= ' [balloon=0]';
99 header
: gettext('Processors'),
101 editor
: caps
.vms
['VM.Config.CPU'] || caps
.vms
['VM.Config.HWType']
102 ? processorEditor
: undefined,
103 tdCls
: 'pve-itype-icon-cpu',
106 multiKey
: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'],
107 renderer: function(value
, metaData
, record
, rowIndex
, colIndex
, store
, pending
) {
108 var sockets
= me
.getObjectValue('sockets', 1, pending
);
109 var model
= me
.getObjectValue('cpu', undefined, pending
);
110 var cores
= me
.getObjectValue('cores', 1, pending
);
111 var numa
= me
.getObjectValue('numa', undefined, pending
);
112 var vcpus
= me
.getObjectValue('vcpus', undefined, pending
);
113 var cpulimit
= me
.getObjectValue('cpulimit', undefined, pending
);
114 var cpuunits
= me
.getObjectValue('cpuunits', undefined, pending
);
116 let res
= Ext
.String
.format(
117 '{0} ({1} sockets, {2} cores)', sockets
* cores
, sockets
, cores
);
120 res
+= ' [' + model
+ ']';
123 res
+= ' [numa=' + numa
+']';
126 res
+= ' [vcpus=' + vcpus
+']';
129 res
+= ' [cpulimit=' + cpulimit
+']';
132 res
+= ' [cpuunits=' + cpuunits
+']';
142 editor
: caps
.vms
['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
144 iconCls
: 'microchip',
145 renderer
: PVE
.Utils
.render_qemu_bios
,
148 header
: gettext('Display'),
149 editor
: caps
.vms
['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
154 renderer
: PVE
.Utils
.render_kvm_vga_driver
,
157 header
: gettext('Machine'),
158 editor
: caps
.vms
['VM.Config.HWType'] ? 'PVE.qemu.MachineEdit' : undefined,
163 renderer: function(value
, metaData
, record
, rowIndex
, colIndex
, store
, pending
) {
164 let ostype
= me
.getObjectValue('ostype', undefined, pending
);
165 if (PVE
.Utils
.is_windows(ostype
) &&
166 (!value
|| value
=== 'pc' || value
=== 'q35')) {
167 return value
=== 'q35' ? 'pc-q35-5.1' : 'pc-i440fx-5.1';
169 return PVE
.Utils
.render_qemu_machine(value
);
173 header
: gettext('SCSI Controller'),
175 editor
: caps
.vms
['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined,
176 renderer
: PVE
.Utils
.render_scsihw
,
182 header
: gettext('Hibernation VM State'),
184 del_extra_msg
: gettext('The saved VM state will be permanently lost.'),
219 PVE
.Utils
.forEachBus(undefined, function(type
, id
) {
220 let confid
= type
+ id
;
224 editor
: 'PVE.qemu.HDEdit',
225 isOnStorageBus
: true,
226 header
: gettext('Hard Disk') + ' (' + confid
+')',
227 cdheader
: gettext('CD/DVD Drive') + ' (' + confid
+')',
228 cloudheader
: gettext('CloudInit Drive') + ' (' + confid
+ ')',
231 for (let i
= 0; i
< PVE
.Utils
.hardware_counts
.net
; i
++) {
232 let confid
= "net" + i
.toString();
237 editor
: caps
.vms
['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
238 never_delete
: !caps
.vms
['VM.Config.Network'],
239 header
: gettext('Network Device') + ' (' + confid
+')',
246 never_delete
: !caps
.vms
['VM.Config.Disk'],
247 header
: gettext('EFI Disk'),
253 never_delete
: !caps
.vms
['VM.Config.Disk'],
254 header
: gettext('TPM State'),
256 for (let i
= 0; i
< PVE
.Utils
.hardware_counts
.usb
; i
++) {
257 let confid
= "usb" + i
.toString();
262 editor
: caps
.nodes
['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined,
263 never_delete
: !caps
.nodes
['Sys.Console'],
264 header
: gettext('USB Device') + ' (' + confid
+ ')',
267 for (let i
= 0; i
< PVE
.Utils
.hardware_counts
.hostpci
; i
++) {
268 let confid
= "hostpci" + i
.toString();
272 tdCls
: 'pve-itype-icon-pci',
273 never_delete
: !caps
.nodes
['Sys.Console'],
274 editor
: caps
.nodes
['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined,
275 header
: gettext('PCI Device') + ' (' + confid
+ ')',
278 for (let i
= 0; i
< PVE
.Utils
.hardware_counts
.serial
; i
++) {
279 let confid
= "serial" + i
.toString();
283 tdCls
: 'pve-itype-icon-serial',
284 never_delete
: !caps
.nodes
['Sys.Console'],
285 header
: gettext('Serial Port') + ' (' + confid
+ ')',
290 iconCls
: 'volume-up',
291 editor
: caps
.vms
['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined,
292 never_delete
: !caps
.vms
['VM.Config.HWType'],
293 header
: gettext('Audio Device'),
295 for (let i
= 0; i
< 256; i
++) {
296 rows
["unused" + i
.toString()] = {
300 del_extra_msg
: gettext('This will permanently erase all data.'),
301 editor
: caps
.vms
['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
302 header
: gettext('Unused Disk') + ' ' + i
.toString(),
307 tdCls
: 'pve-itype-icon-die',
308 editor
: caps
.nodes
['Sys.Console'] ? 'PVE.qemu.RNGEdit' : undefined,
309 never_delete
: !caps
.nodes
['Sys.Console'],
310 header
: gettext("VirtIO RNG"),
313 var sorterFn = function(rec1
, rec2
) {
314 var v1
= rec1
.data
.key
;
315 var v2
= rec2
.data
.key
;
316 var g1
= rows
[v1
].group
|| 0;
317 var g2
= rows
[v2
].group
|| 0;
318 var order1
= rows
[v1
].order
|| 0;
319 var order2
= rows
[v2
].order
|| 0;
325 if (order1
- order2
!== 0) {
326 return order1
- order2
;
331 } else if (v1
< v2
) {
338 let baseurl
= `nodes/${nodename}/qemu/${vmid}/config`;
340 let sm
= Ext
.create('Ext.selection.RowModel', {});
342 let run_editor = function() {
343 let rec
= sm
.getSelection()[0];
344 if (!rec
|| !rows
[rec
.data
.key
]?.editor
) {
347 let rowdef
= rows
[rec
.data
.key
];
348 let editor
= rowdef
.editor
;
350 if (rowdef
.isOnStorageBus
) {
351 let value
= me
.getObjectValue(rec
.data
.key
, '', true);
352 if (isCloudInitKey(value
)) {
354 } else if (value
.match(/media=cdrom/)) {
355 editor
= 'PVE.qemu.CDEdit';
356 } else if (!diskCap
) {
363 pveSelNode
: me
.pveSelNode
,
364 confid
: rec
.data
.key
,
365 url
: `/api2/extjs/${baseurl}`,
367 destroy
: () => me
.reload(),
371 if (Ext
.isString(editor
)) {
372 Ext
.create(editor
, commonOpts
);
374 let win
= Ext
.createWidget(rowdef
.editor
.xtype
, Ext
.apply(commonOpts
, rowdef
.editor
));
379 let edit_btn
= new Proxmox
.button
.Button({
380 text
: gettext('Edit'),
386 let move_menuitem
= new Ext
.menu
.Item({
387 text
: gettext('Move Storage'),
388 tooltip
: gettext('Move disk to another storage'),
389 iconCls
: 'fa fa-database',
392 let rec
= sm
.getSelection()[0];
396 Ext
.create('PVE.window.HDMove', {
403 destroy
: () => me
.reload(),
409 let reassign_menuitem
= new Ext
.menu
.Item({
410 text
: gettext('Reassign Owner'),
411 tooltip
: gettext('Reassign disk to another VM'),
412 iconCls
: 'fa fa-desktop',
415 let rec
= sm
.getSelection()[0];
420 Ext
.create('PVE.window.GuestDiskReassign', {
427 destroy
: () => me
.reload(),
433 let resize_menuitem
= new Ext
.menu
.Item({
434 text
: gettext('Resize'),
435 iconCls
: 'fa fa-plus',
438 let rec
= sm
.getSelection()[0];
442 Ext
.create('PVE.window.HDResize', {
448 destroy
: () => me
.reload(),
454 let diskaction_btn
= new Proxmox
.button
.Button({
455 text
: gettext('Disk Action'),
467 let remove_btn
= new Proxmox
.button
.Button({
468 text
: gettext('Remove'),
469 defaultText
: gettext('Remove'),
470 altText
: gettext('Detach'),
475 confirmMsg: function(rec
) {
476 let warn
= gettext('Are you sure you want to remove entry {0}');
477 if (this.text
=== this.altText
) {
478 warn
= gettext('Are you sure you want to detach entry {0}');
480 let rendered
= me
.renderKey(rec
.data
.key
, {}, rec
);
481 let msg
= Ext
.String
.format(warn
, `'${rendered}'`);
483 if (rows
[rec
.data
.key
].del_extra_msg
) {
484 msg
+= '<br>' + rows
[rec
.data
.key
].del_extra_msg
;
488 handler: function(btn
, e
, rec
) {
489 Proxmox
.Utils
.API2Request({
490 url
: '/api2/extjs/' + baseurl
,
492 method
: btn
.RESTMethod
,
494 'delete': rec
.data
.key
,
496 callback
: () => me
.reload(),
497 failure
: response
=> Ext
.Msg
.alert('Error', response
.htmlStatus
),
498 success: function(response
, options
) {
499 if (btn
.RESTMethod
=== 'POST') {
500 Ext
.create('Proxmox.window.TaskProgress', {
502 upid
: response
.result
.data
,
504 destroy
: () => me
.reload(),
512 render: function(btn
) {
513 // hack: calculate the max button width on first display to prevent the whole
514 // toolbar to move when we switch between the "Remove" and "Detach" labels
515 var def
= btn
.getSize().width
;
517 btn
.setText(btn
.altText
);
518 var alt
= btn
.getSize().width
;
520 btn
.setText(btn
.defaultText
);
522 var optimal
= alt
> def
? alt
: def
;
523 btn
.setSize({ width
: optimal
});
528 let revert_btn
= new PVE
.button
.PendingRevert({
529 apiurl
: '/api2/extjs/' + baseurl
,
532 let efidisk_menuitem
= Ext
.create('Ext.menu.Item', {
533 text
: gettext('EFI Disk'),
534 iconCls
: 'fa fa-fw fa-hdd-o black',
535 disabled
: !caps
.vms
['VM.Config.Disk'],
536 handler: function() {
537 let { data
: bios
} = me
.rstore
.getData().map
.bios
|| {};
539 Ext
.create('PVE.qemu.EFIDiskEdit', {
541 url
: '/api2/extjs/' + baseurl
,
542 pveSelNode
: me
.pveSelNode
,
543 usesEFI
: bios
?.value
=== 'ovmf' || bios
?.pending
=== 'ovmf',
545 destroy
: () => me
.reload(),
552 let isAtLimit
= (type
) => counts
[type
] >= PVE
.Utils
.hardware_counts
[type
];
553 let isAtUsbLimit
= () => {
554 let ostype
= me
.getObjectValue('ostype');
555 let machine
= me
.getObjectValue('machine');
556 return counts
.usb
>= PVE
.Utils
.get_max_usb_count(ostype
, machine
);
559 let set_button_status = function() {
560 let selection_model
= me
.getSelectionModel();
561 let rec
= selection_model
.getSelection()[0];
563 counts
= {}; // en/disable hardwarebuttons
564 let hasCloudInit
= false;
565 me
.rstore
.getData().items
.forEach(function({ id
, data
}) {
566 if (!hasCloudInit
&& (isCloudInitKey(data
.value
) || isCloudInitKey(data
.pending
))) {
571 let match
= id
.match(/^([^\d]+)\d+$/);
572 if (match
&& PVE
.Utils
.hardware_counts
[match
[1]] !== undefined) {
574 counts
[type
] = (counts
[type
] || 0) + 1;
578 // heuristic only for disabling some stuff, the backend has the final word.
579 const noSysConsolePerm
= !caps
.nodes
['Sys.Console'];
580 const noVMConfigHWTypePerm
= !caps
.vms
['VM.Config.HWType'];
581 const noVMConfigNetPerm
= !caps
.vms
['VM.Config.Network'];
582 const noVMConfigDiskPerm
= !caps
.vms
['VM.Config.Disk'];
583 const noVMConfigCDROMPerm
= !caps
.vms
['VM.Config.CDROM'];
584 const noVMConfigCloudinitPerm
= !caps
.vms
['VM.Config.Cloudinit'];
586 me
.down('#addUsb').setDisabled(noSysConsolePerm
|| isAtUsbLimit());
587 me
.down('#addPci').setDisabled(noSysConsolePerm
|| isAtLimit('hostpci'));
588 me
.down('#addAudio').setDisabled(noVMConfigHWTypePerm
|| isAtLimit('audio'));
589 me
.down('#addSerial').setDisabled(noVMConfigHWTypePerm
|| isAtLimit('serial'));
590 me
.down('#addNet').setDisabled(noVMConfigNetPerm
|| isAtLimit('net'));
591 me
.down('#addRng').setDisabled(noSysConsolePerm
|| isAtLimit('rng'));
592 efidisk_menuitem
.setDisabled(noVMConfigDiskPerm
|| isAtLimit('efidisk'));
593 me
.down('#addTpmState').setDisabled(noSysConsolePerm
|| isAtLimit('tpmstate'));
594 me
.down('#addCloudinitDrive').setDisabled(noVMConfigCDROMPerm
|| noVMConfigCloudinitPerm
|| hasCloudInit
);
597 remove_btn
.disable();
599 diskaction_btn
.disable();
600 revert_btn
.disable();
603 const { key
, value
} = rec
.data
;
604 const row
= rows
[key
];
606 const deleted
= !!rec
.data
.delete;
607 const pending
= deleted
|| me
.hasPendingChanges(key
);
609 const isCloudInit
= isCloudInitKey(value
);
610 const isCDRom
= value
&& !!value
.toString().match(/media=cdrom/);
612 const isUnusedDisk
= key
.match(/^unused\d+/);
613 const isUsedDisk
= !isUnusedDisk
&& row
.isOnStorageBus
&& !isCDRom
;
614 const isDisk
= isUnusedDisk
|| isUsedDisk
;
615 const isEfi
= key
=== 'efidisk0';
616 const tpmMoveable
= key
=== 'tpmstate0' && !me
.pveSelNode
.data
.running
;
618 let cannotDelete
= deleted
|| row
.never_delete
;
619 cannotDelete
||= isCDRom
&& !cdromCap
;
620 cannotDelete
||= isDisk
&& !diskCap
;
621 cannotDelete
||= isCloudInit
&& noVMConfigCloudinitPerm
;
622 remove_btn
.setDisabled(cannotDelete
);
624 remove_btn
.setText(isUsedDisk
&& !isCloudInit
? remove_btn
.altText
: remove_btn
.defaultText
);
625 remove_btn
.RESTMethod
= isUnusedDisk
? 'POST':'PUT';
627 edit_btn
.setDisabled(
628 deleted
|| !row
.editor
|| isCloudInit
|| (isCDRom
&& !cdromCap
) || (isDisk
&& !diskCap
));
630 diskaction_btn
.setDisabled(
634 !(isDisk
|| isEfi
|| tpmMoveable
),
636 move_menuitem
.setDisabled(isUnusedDisk
);
637 reassign_menuitem
.setDisabled(pending
|| (isEfi
|| tpmMoveable
));
638 resize_menuitem
.setDisabled(pending
|| !isUsedDisk
);
640 revert_btn
.setDisabled(!pending
);
643 let editorFactory
= (classPath
, extraOptions
) => {
644 extraOptions
= extraOptions
|| {};
645 return () => Ext
.create(`PVE.qemu.${classPath}`, {
647 url
: `/api2/extjs/${baseurl}`,
648 pveSelNode
: me
.pveSelNode
,
650 destroy
: () => me
.reload(),
659 url
: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`,
662 run_editor
: run_editor
,
665 text
: gettext('Add'),
666 menu
: new Ext
.menu
.Menu({
667 cls
: 'pve-add-hw-menu',
670 text
: gettext('Hard Disk'),
671 iconCls
: 'fa fa-fw fa-hdd-o black',
672 disabled
: !caps
.vms
['VM.Config.Disk'],
673 handler
: editorFactory('HDEdit'),
676 text
: gettext('CD/DVD Drive'),
677 iconCls
: 'pve-itype-icon-cdrom',
678 disabled
: !caps
.vms
['VM.Config.CDROM'],
679 handler
: editorFactory('CDEdit'),
682 text
: gettext('Network Device'),
684 iconCls
: 'fa fa-fw fa-exchange black',
685 disabled
: !caps
.vms
['VM.Config.Network'],
686 handler
: editorFactory('NetworkEdit'),
690 text
: gettext('TPM State'),
691 itemId
: 'addTpmState',
692 iconCls
: 'fa fa-fw fa-hdd-o black',
693 disabled
: !caps
.vms
['VM.Config.Disk'],
694 handler
: editorFactory('TPMDiskEdit'),
697 text
: gettext('USB Device'),
699 iconCls
: 'fa fa-fw fa-usb black',
700 disabled
: !caps
.nodes
['Sys.Console'],
701 handler
: editorFactory('USBEdit'),
704 text
: gettext('PCI Device'),
706 iconCls
: 'pve-itype-icon-pci',
707 disabled
: !caps
.nodes
['Sys.Console'],
708 handler
: editorFactory('PCIEdit'),
711 text
: gettext('Serial Port'),
713 iconCls
: 'pve-itype-icon-serial',
714 disabled
: !caps
.vms
['VM.Config.Options'],
715 handler
: editorFactory('SerialEdit'),
718 text
: gettext('CloudInit Drive'),
719 itemId
: 'addCloudinitDrive',
720 iconCls
: 'fa fa-fw fa-cloud black',
721 disabled
: !caps
.vms
['VM.Config.CDROM'] || !caps
.vms
['VM.Config.Cloudinit'],
722 handler
: editorFactory('CIDriveEdit'),
725 text
: gettext('Audio Device'),
727 iconCls
: 'fa fa-fw fa-volume-up black',
728 disabled
: !caps
.vms
['VM.Config.HWType'],
729 handler
: editorFactory('AudioEdit'),
732 text
: gettext("VirtIO RNG"),
734 iconCls
: 'pve-itype-icon-die',
735 disabled
: !caps
.nodes
['Sys.Console'],
736 handler
: editorFactory('RNGEdit'),
749 itemdblclick
: run_editor
,
750 selectionchange
: set_button_status
,
756 me
.on('activate', me
.rstore
.startUpdate
, me
.rstore
);
757 me
.on('destroy', me
.rstore
.stopUpdate
, me
.rstore
);
759 me
.mon(me
.getStore(), 'datachanged', set_button_status
, me
);