]>
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 nodename
= me
.pveSelNode
.data
.node
;
51 throw "no node name specified";
54 const vmid
= me
.pveSelNode
.data
.vmid
;
56 throw "no VM ID specified";
59 const caps
= Ext
.state
.Manager
.get('GuiCap');
60 const diskCap
= caps
.vms
['VM.Config.Disk'];
61 const cdromCap
= caps
.vms
['VM.Config.CDROM'];
65 header
: gettext('Memory'),
66 editor
: caps
.vms
['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
69 tdCls
: 'pmx-itype-icon-memory',
71 multiKey
: ['memory', 'balloon', 'shares'],
72 renderer: function(value
, metaData
, record
, ri
, ci
, store
, pending
) {
75 var max
= me
.getObjectValue('memory', 512, pending
);
76 var balloon
= me
.getObjectValue('balloon', undefined, pending
);
77 var shares
= me
.getObjectValue('shares', undefined, pending
);
79 res
= Proxmox
.Utils
.format_size(max
*1024*1024);
81 if (balloon
!== undefined && balloon
> 0) {
82 res
= Proxmox
.Utils
.format_size(balloon
*1024*1024) + "/" + res
;
85 res
+= ' [shares=' + shares
+']';
87 } else if (balloon
=== 0) {
88 res
+= ' [balloon=0]';
94 header
: gettext('Processors'),
96 editor
: caps
.vms
['VM.Config.CPU'] || caps
.vms
['VM.Config.HWType']
97 ? 'PVE.qemu.ProcessorEdit' : undefined,
98 tdCls
: 'pve-itype-icon-cpu',
101 multiKey
: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'],
102 renderer: function(value
, metaData
, record
, rowIndex
, colIndex
, store
, pending
) {
103 var sockets
= me
.getObjectValue('sockets', 1, pending
);
104 var model
= me
.getObjectValue('cpu', undefined, pending
);
105 var cores
= me
.getObjectValue('cores', 1, pending
);
106 var numa
= me
.getObjectValue('numa', undefined, pending
);
107 var vcpus
= me
.getObjectValue('vcpus', undefined, pending
);
108 var cpulimit
= me
.getObjectValue('cpulimit', undefined, pending
);
109 var cpuunits
= me
.getObjectValue('cpuunits', undefined, pending
);
111 var res
= Ext
.String
.format('{0} ({1} sockets, {2} cores)',
112 sockets
*cores
, sockets
, cores
);
115 res
+= ' [' + model
+ ']';
119 res
+= ' [numa=' + numa
+']';
123 res
+= ' [vcpus=' + vcpus
+']';
127 res
+= ' [cpulimit=' + cpulimit
+']';
131 res
+= ' [cpuunits=' + cpuunits
+']';
141 editor
: caps
.vms
['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
143 iconCls
: 'microchip',
144 renderer
: PVE
.Utils
.render_qemu_bios
,
147 header
: gettext('Display'),
148 editor
: caps
.vms
['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
153 renderer
: PVE
.Utils
.render_kvm_vga_driver
,
156 header
: gettext('Machine'),
157 editor
: caps
.vms
['VM.Config.HWType'] ? 'PVE.qemu.MachineEdit' : undefined,
162 renderer: function(value
, metaData
, record
, rowIndex
, colIndex
, store
, pending
) {
163 let ostype
= me
.getObjectValue('ostype', undefined, pending
);
164 if (PVE
.Utils
.is_windows(ostype
) &&
165 (!value
|| value
=== 'pc' || value
=== 'q35')) {
166 return value
=== 'q35' ? 'pc-q35-5.1' : 'pc-i440fx-5.1';
168 return PVE
.Utils
.render_qemu_machine(value
);
172 header
: gettext('SCSI Controller'),
174 editor
: caps
.vms
['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined,
175 renderer
: PVE
.Utils
.render_scsihw
,
181 header
: gettext('Hibernation VM State'),
183 del_extra_msg
: gettext('The saved VM state will be permanently lost.'),
218 PVE
.Utils
.forEachBus(undefined, function(type
, id
) {
219 let confid
= type
+ id
;
223 editor
: 'PVE.qemu.HDEdit',
224 isOnStorageBus
: true,
225 header
: gettext('Hard Disk') + ' (' + confid
+')',
226 cdheader
: gettext('CD/DVD Drive') + ' (' + confid
+')',
227 cloudheader
: gettext('CloudInit Drive') + ' (' + confid
+ ')',
230 for (let i
= 0; i
< PVE
.Utils
.hardware_counts
.net
; i
++) {
231 let confid
= "net" + i
.toString();
236 editor
: caps
.vms
['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
237 never_delete
: !caps
.vms
['VM.Config.Network'],
238 header
: gettext('Network Device') + ' (' + confid
+')',
245 never_delete
: !caps
.vms
['VM.Config.Disk'],
246 header
: gettext('EFI Disk'),
252 never_delete
: !caps
.vms
['VM.Config.Disk'],
253 header
: gettext('TPM State'),
255 for (let i
= 0; i
< PVE
.Utils
.hardware_counts
.usb
; i
++) {
256 let confid
= "usb" + i
.toString();
261 editor
: caps
.nodes
['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined,
262 never_delete
: !caps
.nodes
['Sys.Console'],
263 header
: gettext('USB Device') + ' (' + confid
+ ')',
266 for (let i
= 0; i
< PVE
.Utils
.hardware_counts
.hostpci
; i
++) {
267 let confid
= "hostpci" + i
.toString();
271 tdCls
: 'pve-itype-icon-pci',
272 never_delete
: !caps
.nodes
['Sys.Console'],
273 editor
: caps
.nodes
['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined,
274 header
: gettext('PCI Device') + ' (' + confid
+ ')',
277 for (let i
= 0; i
< PVE
.Utils
.hardware_counts
.serial
; i
++) {
278 let confid
= "serial" + i
.toString();
282 tdCls
: 'pve-itype-icon-serial',
283 never_delete
: !caps
.nodes
['Sys.Console'],
284 header
: gettext('Serial Port') + ' (' + confid
+ ')',
289 iconCls
: 'volume-up',
290 editor
: caps
.vms
['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined,
291 never_delete
: !caps
.vms
['VM.Config.HWType'],
292 header
: gettext('Audio Device'),
294 for (let i
= 0; i
< 256; i
++) {
295 rows
["unused" + i
.toString()] = {
299 del_extra_msg
: gettext('This will permanently erase all data.'),
300 editor
: caps
.vms
['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
301 header
: gettext('Unused Disk') + ' ' + i
.toString(),
306 tdCls
: 'pve-itype-icon-die',
307 editor
: caps
.nodes
['Sys.Console'] ? 'PVE.qemu.RNGEdit' : undefined,
308 never_delete
: !caps
.nodes
['Sys.Console'],
309 header
: gettext("VirtIO RNG"),
312 var sorterFn = function(rec1
, rec2
) {
313 var v1
= rec1
.data
.key
;
314 var v2
= rec2
.data
.key
;
315 var g1
= rows
[v1
].group
|| 0;
316 var g2
= rows
[v2
].group
|| 0;
317 var order1
= rows
[v1
].order
|| 0;
318 var order2
= rows
[v2
].order
|| 0;
324 if (order1
- order2
!== 0) {
325 return order1
- order2
;
330 } else if (v1
< v2
) {
337 var baseurl
= 'nodes/' + nodename
+ '/qemu/' + vmid
+ '/config';
339 var sm
= Ext
.create('Ext.selection.RowModel', {});
341 var run_editor = function() {
342 var rec
= sm
.getSelection()[0];
347 var rowdef
= rows
[rec
.data
.key
];
348 if (!rowdef
.editor
) {
352 var editor
= rowdef
.editor
;
353 if (rowdef
.isOnStorageBus
) {
354 var value
= me
.getObjectValue(rec
.data
.key
, '', true);
355 if (value
.match(/vm-.*-cloudinit/)) {
357 } else if (value
.match(/media=cdrom/)) {
358 editor
= 'PVE.qemu.CDEdit';
359 } else if (!diskCap
) {
366 if (Ext
.isString(editor
)) {
367 win
= Ext
.create(editor
, {
368 pveSelNode
: me
.pveSelNode
,
369 confid
: rec
.data
.key
,
370 url
: '/api2/extjs/' + baseurl
,
373 var config
= Ext
.apply({
374 pveSelNode
: me
.pveSelNode
,
375 confid
: rec
.data
.key
,
376 url
: '/api2/extjs/' + baseurl
,
378 win
= Ext
.createWidget(rowdef
.editor
.xtype
, config
);
383 win
.on('destroy', me
.reload
, me
);
386 var run_resize = function() {
387 var rec
= sm
.getSelection()[0];
392 var win
= Ext
.create('PVE.window.HDResize', {
400 win
.on('destroy', me
.reload
, me
);
403 var run_move = function() {
404 var rec
= sm
.getSelection()[0];
409 var win
= Ext
.create('PVE.window.HDMove', {
417 win
.on('destroy', me
.reload
, me
);
420 var edit_btn
= new Proxmox
.button
.Button({
421 text
: gettext('Edit'),
427 var resize_btn
= new Proxmox
.button
.Button({
428 text
: gettext('Resize disk'),
434 var move_btn
= new Proxmox
.button
.Button({
435 text
: gettext('Move disk'),
441 var remove_btn
= new Proxmox
.button
.Button({
442 text
: gettext('Remove'),
443 defaultText
: gettext('Remove'),
444 altText
: gettext('Detach'),
449 confirmMsg: function(rec
) {
450 let warn
= gettext('Are you sure you want to remove entry {0}');
451 if (this.text
=== this.altText
) {
452 warn
= gettext('Are you sure you want to detach entry {0}');
454 let rendered
= me
.renderKey(rec
.data
.key
, {}, rec
);
455 let msg
= Ext
.String
.format(warn
, `'${rendered}'`);
457 if (rows
[rec
.data
.key
].del_extra_msg
) {
458 msg
+= '<br>' + rows
[rec
.data
.key
].del_extra_msg
;
462 handler: function(b
, e
, rec
) {
463 Proxmox
.Utils
.API2Request({
464 url
: '/api2/extjs/' + baseurl
,
466 method
: b
.RESTMethod
,
468 'delete': rec
.data
.key
,
470 callback
: () => me
.reload(),
471 failure: function(response
, opts
) {
472 Ext
.Msg
.alert('Error', response
.htmlStatus
);
474 success: function(response
, options
) {
475 if (b
.RESTMethod
=== 'POST') {
476 var upid
= response
.result
.data
;
477 var win
= Ext
.create('Proxmox.window.TaskProgress', {
480 destroy
: () => me
.reload(),
489 render: function(btn
) {
490 // hack: calculate the max button width on first display to prevent the whole
491 // toolbar to move when we switch between the "Remove" and "Detach" labels
492 var def
= btn
.getSize().width
;
494 btn
.setText(btn
.altText
);
495 var alt
= btn
.getSize().width
;
497 btn
.setText(btn
.defaultText
);
499 var optimal
= alt
> def
? alt
: def
;
500 btn
.setSize({ width
: optimal
});
505 var revert_btn
= new PVE
.button
.PendingRevert({
506 apiurl
: '/api2/extjs/' + baseurl
,
509 var efidisk_menuitem
= Ext
.create('Ext.menu.Item', {
510 text
: gettext('EFI Disk'),
511 iconCls
: 'fa fa-fw fa-hdd-o black',
512 disabled
: !caps
.vms
['VM.Config.Disk'],
513 handler: function() {
514 let bios
= me
.rstore
.getData().map
.bios
;
515 let usesEFI
= bios
&& (bios
.data
.value
=== 'ovmf' || bios
.data
.pending
=== 'ovmf');
517 var win
= Ext
.create('PVE.qemu.EFIDiskEdit', {
518 url
: '/api2/extjs/' + baseurl
,
519 pveSelNode
: me
.pveSelNode
,
522 win
.on('destroy', me
.reload
, me
);
528 let isAtLimit
= (type
) => counts
[type
] >= PVE
.Utils
.hardware_counts
[type
];
530 var set_button_status = function() {
531 var selection_model
= me
.getSelectionModel();
532 var rec
= selection_model
.getSelection()[0];
534 // en/disable hardwarebuttons
536 var hasCloudInit
= false;
537 me
.rstore
.getData().items
.forEach(function(item
) {
538 if (!hasCloudInit
&& (
539 /vm-.*-cloudinit/.test(item
.data
.value
) ||
540 /vm-.*-cloudinit/.test(item
.data
.pending
)
546 let match
= item
.id
.match(/^([^\d]+)\d+$/);
548 if (match
&& PVE
.Utils
.hardware_counts
[match
[1]] !== undefined) {
554 counts
[type
] = (counts
[type
] || 0) + 1;
557 // heuristic only for disabling some stuff, the backend has the final word.
558 const noSysConsolePerm
= !caps
.nodes
['Sys.Console'];
559 const noVMConfigHWTypePerm
= !caps
.vms
['VM.Config.HWType'];
560 const noVMConfigNetPerm
= !caps
.vms
['VM.Config.Network'];
561 const noVMConfigDiskPerm
= !caps
.vms
['VM.Config.Disk'];
563 me
.down('#addusb').setDisabled(noSysConsolePerm
|| isAtLimit('usb'));
564 me
.down('#addpci').setDisabled(noSysConsolePerm
|| isAtLimit('hostpci'));
565 me
.down('#addaudio').setDisabled(noVMConfigHWTypePerm
|| isAtLimit('audio'));
566 me
.down('#addserial').setDisabled(noVMConfigHWTypePerm
|| isAtLimit('serial'));
567 me
.down('#addnet').setDisabled(noVMConfigNetPerm
|| isAtLimit('net'));
568 me
.down('#addrng').setDisabled(noSysConsolePerm
|| isAtLimit('rng'));
569 efidisk_menuitem
.setDisabled(noVMConfigDiskPerm
|| isAtLimit('efidisk'));
570 me
.down('#addtpmstate').setDisabled(noSysConsolePerm
|| isAtLimit('tpmstate'));
571 me
.down('#addci').setDisabled(noSysConsolePerm
|| hasCloudInit
);
574 remove_btn
.disable();
576 resize_btn
.disable();
578 revert_btn
.disable();
581 const key
= rec
.data
.key
;
582 const value
= rec
.data
.value
;
583 const row
= rows
[key
];
585 const deleted
= !!rec
.data
.delete;
586 const pending
= deleted
|| me
.hasPendingChanges(key
);
588 const isCloudInit
= value
&& value
.toString().match(/vm-.*-cloudinit/);
589 const isCDRom
= value
&& !!value
.toString().match(/media=cdrom/) && !isCloudInit
;
591 const isUnusedDisk
= key
.match(/^unused\d+/);
592 const isUsedDisk
= !isUnusedDisk
&& row
.isOnStorageBus
&& !isCDRom
&& !isCloudInit
;
593 const isDisk
= isCloudInit
|| isUnusedDisk
|| isUsedDisk
;
594 const isEfi
= key
=== 'efidisk0';
595 const tpmMoveable
= key
=== 'tpmstate0' && !me
.pveSelNode
.data
.running
;
597 remove_btn
.setDisabled(
600 (isCDRom
&& !cdromCap
) ||
601 (isDisk
&& !diskCap
),
603 remove_btn
.setText(isUsedDisk
&& !isCloudInit
? remove_btn
.altText
: remove_btn
.defaultText
);
604 remove_btn
.RESTMethod
= isUnusedDisk
? 'POST':'PUT';
606 edit_btn
.setDisabled(
610 (isCDRom
&& !cdromCap
) ||
611 (isDisk
&& !diskCap
),
614 resize_btn
.setDisabled(pending
|| !isUsedDisk
|| !diskCap
);
616 move_btn
.setDisabled(pending
|| !(isUsedDisk
|| isEfi
|| tpmMoveable
) || !diskCap
);
618 revert_btn
.setDisabled(!pending
);
622 url
: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`,
625 run_editor
: run_editor
,
628 text
: gettext('Add'),
629 menu
: new Ext
.menu
.Menu({
630 cls
: 'pve-add-hw-menu',
633 text
: gettext('Hard Disk'),
634 iconCls
: 'fa fa-fw fa-hdd-o black',
635 disabled
: !caps
.vms
['VM.Config.Disk'],
636 handler: function() {
637 let win
= Ext
.create('PVE.qemu.HDEdit', {
638 url
: '/api2/extjs/' + baseurl
,
639 pveSelNode
: me
.pveSelNode
,
641 win
.on('destroy', me
.reload
, me
);
646 text
: gettext('CD/DVD Drive'),
647 iconCls
: 'pve-itype-icon-cdrom',
648 disabled
: !caps
.vms
['VM.Config.CDROM'],
649 handler: function() {
650 let win
= Ext
.create('PVE.qemu.CDEdit', {
651 url
: '/api2/extjs/' + baseurl
,
652 pveSelNode
: me
.pveSelNode
,
654 win
.on('destroy', me
.reload
, me
);
659 text
: gettext('Network Device'),
661 iconCls
: 'fa fa-fw fa-exchange black',
662 disabled
: !caps
.vms
['VM.Config.Network'],
663 handler: function() {
664 var win
= Ext
.create('PVE.qemu.NetworkEdit', {
665 url
: '/api2/extjs/' + baseurl
,
666 pveSelNode
: me
.pveSelNode
,
669 win
.on('destroy', me
.reload
, me
);
675 text
: gettext('TPM State'),
676 itemId
: 'addtpmstate',
677 iconCls
: 'fa fa-fw fa-hdd-o black',
678 disabled
: !caps
.vms
['VM.Config.Disk'],
679 handler: function() {
680 var win
= Ext
.create('PVE.qemu.TPMDiskEdit', {
681 url
: '/api2/extjs/' + baseurl
,
682 pveSelNode
: me
.pveSelNode
,
684 win
.on('destroy', me
.reload
, me
);
689 text
: gettext('USB Device'),
691 iconCls
: 'fa fa-fw fa-usb black',
692 disabled
: !caps
.nodes
['Sys.Console'],
693 handler: function() {
694 var win
= Ext
.create('PVE.qemu.USBEdit', {
695 url
: '/api2/extjs/' + baseurl
,
696 pveSelNode
: me
.pveSelNode
,
698 win
.on('destroy', me
.reload
, me
);
703 text
: gettext('PCI Device'),
705 iconCls
: 'pve-itype-icon-pci',
706 disabled
: !caps
.nodes
['Sys.Console'],
707 handler: function() {
708 var win
= Ext
.create('PVE.qemu.PCIEdit', {
709 url
: '/api2/extjs/' + baseurl
,
710 pveSelNode
: me
.pveSelNode
,
712 win
.on('destroy', me
.reload
, me
);
717 text
: gettext('Serial Port'),
719 iconCls
: 'pve-itype-icon-serial',
720 disabled
: !caps
.vms
['VM.Config.Options'],
721 handler: function() {
722 var win
= Ext
.create('PVE.qemu.SerialEdit', {
723 url
: '/api2/extjs/' + baseurl
,
725 win
.on('destroy', me
.reload
, me
);
730 text
: gettext('CloudInit Drive'),
732 iconCls
: 'fa fa-fw fa-cloud black',
733 disabled
: !caps
.nodes
['Sys.Console'],
734 handler: function() {
735 var win
= Ext
.create('PVE.qemu.CIDriveEdit', {
736 url
: '/api2/extjs/' + baseurl
,
737 pveSelNode
: me
.pveSelNode
,
739 win
.on('destroy', me
.reload
, me
);
744 text
: gettext('Audio Device'),
746 iconCls
: 'fa fa-fw fa-volume-up black',
747 disabled
: !caps
.vms
['VM.Config.HWType'],
748 handler: function() {
749 var win
= Ext
.create('PVE.qemu.AudioEdit', {
750 url
: '/api2/extjs/' + baseurl
,
754 win
.on('destroy', me
.reload
, me
);
759 text
: gettext("VirtIO RNG"),
761 iconCls
: 'pve-itype-icon-die',
762 disabled
: !caps
.nodes
['Sys.Console'],
763 handler: function() {
764 var win
= Ext
.create('PVE.qemu.RNGEdit', {
765 url
: '/api2/extjs/' + baseurl
,
769 win
.on('destroy', me
.reload
, me
);
785 itemdblclick
: run_editor
,
786 selectionchange
: set_button_status
,
792 me
.on('activate', me
.rstore
.startUpdate
, me
.rstore
);
793 me
.on('destroy', me
.rstore
.stopUpdate
, me
.rstore
);
795 me
.mon(me
.getStore(), 'datachanged', set_button_status
, me
);