]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/qemu/HardwareView.js
gui: qemu HardwareView: specify hardware counts in Utils
[pve-manager.git] / www / manager6 / qemu / HardwareView.js
1 Ext.define('PVE.qemu.HardwareView', {
2 extend: 'Proxmox.grid.PendingObjectGrid',
3 alias: ['widget.PVE.qemu.HardwareView'],
4
5 onlineHelp: 'qm_virtual_machines_settings',
6
7 renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
8 var me = this;
9 var rows = me.rows;
10 var rowdef = rows[key] || {};
11 var iconCls = rowdef.iconCls;
12 var icon = '';
13 var txt = (rowdef.header || key);
14
15 metaData.tdAttr = "valign=middle";
16
17 if (rowdef.isOnStorageBus) {
18 var value = me.getObjectValue(key, '', false);
19 if (value === '') {
20 value = me.getObjectValue(key, '', true);
21 }
22 if (value.match(/vm-.*-cloudinit/)) {
23 iconCls = 'cloud';
24 txt = rowdef.cloudheader;
25 } else if (value.match(/media=cdrom/)) {
26 metaData.tdCls = 'pve-itype-icon-cdrom';
27 return rowdef.cdheader;
28 }
29 }
30
31 if (rowdef.tdCls) {
32 metaData.tdCls = rowdef.tdCls;
33 } else if (iconCls) {
34 icon = "<i class='pve-grid-fa fa fa-fw fa-" + iconCls + "'></i>";
35 metaData.tdCls += " pve-itype-fa";
36 }
37
38 // only return icons in grid but not remove dialog
39 if (rowIndex !== undefined) {
40 return icon + txt;
41 } else {
42 return txt;
43 }
44 },
45
46 initComponent : function() {
47 var me = this;
48 var i, confid;
49
50 var nodename = me.pveSelNode.data.node;
51 if (!nodename) {
52 throw "no node name specified";
53 }
54
55 var vmid = me.pveSelNode.data.vmid;
56 if (!vmid) {
57 throw "no VM ID specified";
58 }
59
60 var caps = Ext.state.Manager.get('GuiCap');
61 var diskCap = caps.vms['VM.Config.Disk'];
62
63 /*jslint confusion: true */
64 var rows = {
65 memory: {
66 header: gettext('Memory'),
67 editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
68 never_delete: true,
69 defaultValue: '512',
70 tdCls: 'pve-itype-icon-memory',
71 group: 2,
72 multiKey: ['memory', 'balloon', 'shares'],
73 renderer: function(value, metaData, record, ri, ci, store, pending) {
74 var res = '';
75
76 var max = me.getObjectValue('memory', 512, pending);
77 var balloon = me.getObjectValue('balloon', undefined, pending);
78 var shares = me.getObjectValue('shares', undefined, pending);
79
80 res = Proxmox.Utils.format_size(max*1024*1024);
81
82 if (balloon !== undefined && balloon > 0) {
83 res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res;
84
85 if (shares) {
86 res += ' [shares=' + shares +']';
87 }
88 } else if (balloon === 0) {
89 res += ' [balloon=0]';
90 }
91 return res;
92 }
93 },
94 sockets: {
95 header: gettext('Processors'),
96 never_delete: true,
97 editor: (caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']) ?
98 'PVE.qemu.ProcessorEdit' : undefined,
99 tdCls: 'pve-itype-icon-processor',
100 group: 3,
101 defaultValue: '1',
102 multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'],
103 renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
104
105 var sockets = me.getObjectValue('sockets', 1, pending);
106 var model = me.getObjectValue('cpu', undefined, pending);
107 var cores = me.getObjectValue('cores', 1, pending);
108 var numa = me.getObjectValue('numa', undefined, pending);
109 var vcpus = me.getObjectValue('vcpus', undefined, pending);
110 var cpulimit = me.getObjectValue('cpulimit', undefined, pending);
111 var cpuunits = me.getObjectValue('cpuunits', undefined, pending);
112
113 var res = Ext.String.format('{0} ({1} sockets, {2} cores)',
114 sockets*cores, sockets, cores);
115
116 if (model) {
117 res += ' [' + model + ']';
118 }
119
120 if (numa) {
121 res += ' [numa=' + numa +']';
122 }
123
124 if (vcpus) {
125 res += ' [vcpus=' + vcpus +']';
126 }
127
128 if (cpulimit) {
129 res += ' [cpulimit=' + cpulimit +']';
130 }
131
132 if (cpuunits) {
133 res += ' [cpuunits=' + cpuunits +']';
134 }
135
136 return res;
137 }
138 },
139 bios: {
140 header: 'BIOS',
141 group: 4,
142 never_delete: true,
143 editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
144 defaultValue: '',
145 iconCls: 'microchip',
146 renderer: PVE.Utils.render_qemu_bios
147 },
148 vga: {
149 header: gettext('Display'),
150 editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
151 never_delete: true,
152 iconCls: 'desktop',
153 group:5,
154 defaultValue: '',
155 renderer: PVE.Utils.render_kvm_vga_driver
156 },
157 machine: {
158 header: gettext('Machine'),
159 editor: caps.vms['VM.Config.HWType'] ? {
160 xtype: 'proxmoxWindowEdit',
161 subject: gettext('Machine'),
162 width: 350,
163 items: [{
164 xtype: 'proxmoxKVComboBox',
165 name: 'machine',
166 value: '__default__',
167 fieldLabel: gettext('Machine'),
168 comboItems: [
169 ['__default__', PVE.Utils.render_qemu_machine('')],
170 ['q35', 'q35']
171 ]
172 }]} : undefined,
173 iconCls: 'cogs',
174 never_delete: true,
175 group: 6,
176 defaultValue: '',
177 renderer: PVE.Utils.render_qemu_machine
178 },
179 scsihw: {
180 header: gettext('SCSI Controller'),
181 iconCls: 'database',
182 editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined,
183 renderer: PVE.Utils.render_scsihw,
184 group: 7,
185 never_delete: true,
186 defaultValue: ''
187 },
188 vmstate: {
189 header: gettext('Hibernation VM State'),
190 iconCls: 'download',
191 del_extra_msg: gettext('The saved VM state will be permanently lost.'),
192 group: 100,
193 },
194 cores: {
195 visible: false
196 },
197 cpu: {
198 visible: false
199 },
200 numa: {
201 visible: false
202 },
203 balloon: {
204 visible: false
205 },
206 hotplug: {
207 visible: false
208 },
209 vcpus: {
210 visible: false
211 },
212 cpuunits: {
213 visible: false
214 },
215 cpulimit: {
216 visible: false
217 },
218 shares: {
219 visible: false
220 }
221 };
222 /*jslint confusion: false */
223
224 PVE.Utils.forEachBus(undefined, function(type, id) {
225 var confid = type + id;
226 rows[confid] = {
227 group: 10,
228 iconCls: 'hdd-o',
229 editor: 'PVE.qemu.HDEdit',
230 never_delete: caps.vms['VM.Config.Disk'] ? false : true,
231 isOnStorageBus: true,
232 header: gettext('Hard Disk') + ' (' + confid +')',
233 cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
234 cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')'
235 };
236 });
237 for (i = 0; i < PVE.Utils.hardware_counts.net; i++) {
238 confid = "net" + i.toString();
239 rows[confid] = {
240 group: 15,
241 order: i,
242 iconCls: 'exchange',
243 editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
244 never_delete: caps.vms['VM.Config.Network'] ? false : true,
245 header: gettext('Network Device') + ' (' + confid +')'
246 };
247 }
248 rows.efidisk0 = {
249 group: 20,
250 iconCls: 'hdd-o',
251 editor: null,
252 never_delete: caps.vms['VM.Config.Disk'] ? false : true,
253 header: gettext('EFI Disk')
254 };
255 for (i = 0; i < PVE.Utils.hardware_counts.usb; i++) {
256 confid = "usb" + i.toString();
257 rows[confid] = {
258 group: 25,
259 order: i,
260 iconCls: 'usb',
261 editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined,
262 never_delete: caps.nodes['Sys.Console'] ? false : true,
263 header: gettext('USB Device') + ' (' + confid + ')'
264 };
265 }
266 for (i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
267 confid = "hostpci" + i.toString();
268 rows[confid] = {
269 group: 30,
270 order: i,
271 tdCls: 'pve-itype-icon-pci',
272 never_delete: caps.nodes['Sys.Console'] ? false : true,
273 editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined,
274 header: gettext('PCI Device') + ' (' + confid + ')'
275 };
276 }
277 for (i = 0; i < PVE.Utils.hardware_counts.serial; i++) {
278 confid = "serial" + i.toString();
279 rows[confid] = {
280 group: 35,
281 order: i,
282 tdCls: 'pve-itype-icon-serial',
283 never_delete: caps.nodes['Sys.Console'] ? false : true,
284 header: gettext('Serial Port') + ' (' + confid + ')'
285 };
286 }
287 rows.audio0 = {
288 group: 40,
289 iconCls: 'volume-up',
290 editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined,
291 never_delete: caps.vms['VM.Config.HWType'] ? false : true,
292 header: gettext('Audio Device')
293 };
294 for (i = 0; i < 256; i++) {
295 rows["unused" + i.toString()] = {
296 group: 99,
297 order: i,
298 iconCls: 'hdd-o',
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()
302 };
303 }
304
305 var sorterFn = function(rec1, rec2) {
306 var v1 = rec1.data.key;
307 var v2 = rec2.data.key;
308 var g1 = rows[v1].group || 0;
309 var g2 = rows[v2].group || 0;
310 var order1 = rows[v1].order || 0;
311 var order2 = rows[v2].order || 0;
312
313 if ((g1 - g2) !== 0) {
314 return g1 - g2;
315 }
316
317 if ((order1 - order2) !== 0) {
318 return order1 - order2;
319 }
320
321 if (v1 > v2) {
322 return 1;
323 } else if (v1 < v2) {
324 return -1;
325 } else {
326 return 0;
327 }
328 };
329
330 var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
331
332 var sm = Ext.create('Ext.selection.RowModel', {});
333
334 var run_editor = function() {
335 var rec = sm.getSelection()[0];
336 if (!rec) {
337 return;
338 }
339
340 var rowdef = rows[rec.data.key];
341 if (!rowdef.editor) {
342 return;
343 }
344
345 var editor = rowdef.editor;
346 if (rowdef.isOnStorageBus) {
347 var value = me.getObjectValue(rec.data.key, '', true);
348 if (value.match(/vm-.*-cloudinit/)) {
349 return;
350 } else if (value.match(/media=cdrom/)) {
351 editor = 'PVE.qemu.CDEdit';
352 } else if (!diskCap) {
353 return;
354 }
355 }
356
357 var win;
358
359 if (Ext.isString(editor)) {
360 win = Ext.create(editor, {
361 pveSelNode: me.pveSelNode,
362 confid: rec.data.key,
363 url: '/api2/extjs/' + baseurl
364 });
365 } else {
366 var config = Ext.apply({
367 pveSelNode: me.pveSelNode,
368 confid: rec.data.key,
369 url: '/api2/extjs/' + baseurl
370 }, rowdef.editor);
371 win = Ext.createWidget(rowdef.editor.xtype, config);
372 win.load();
373 }
374
375 win.show();
376 win.on('destroy', me.reload, me);
377 };
378
379 var run_resize = function() {
380 var rec = sm.getSelection()[0];
381 if (!rec) {
382 return;
383 }
384
385 var win = Ext.create('PVE.window.HDResize', {
386 disk: rec.data.key,
387 nodename: nodename,
388 vmid: vmid
389 });
390
391 win.show();
392
393 win.on('destroy', me.reload, me);
394 };
395
396 var run_move = function() {
397 var rec = sm.getSelection()[0];
398 if (!rec) {
399 return;
400 }
401
402 var win = Ext.create('PVE.window.HDMove', {
403 disk: rec.data.key,
404 nodename: nodename,
405 vmid: vmid
406 });
407
408 win.show();
409
410 win.on('destroy', me.reload, me);
411 };
412
413 var edit_btn = new Proxmox.button.Button({
414 text: gettext('Edit'),
415 selModel: sm,
416 disabled: true,
417 handler: run_editor
418 });
419
420 var resize_btn = new Proxmox.button.Button({
421 text: gettext('Resize disk'),
422 selModel: sm,
423 disabled: true,
424 handler: run_resize
425 });
426
427 var move_btn = new Proxmox.button.Button({
428 text: gettext('Move disk'),
429 selModel: sm,
430 disabled: true,
431 handler: run_move
432 });
433
434 var remove_btn = new Proxmox.button.Button({
435 text: gettext('Remove'),
436 defaultText: gettext('Remove'),
437 altText: gettext('Detach'),
438 selModel: sm,
439 disabled: true,
440 dangerous: true,
441 RESTMethod: 'PUT',
442 confirmMsg: function(rec) {
443 var warn = gettext('Are you sure you want to remove entry {0}');
444 if (this.text === this.altText) {
445 warn = gettext('Are you sure you want to detach entry {0}');
446 }
447 var key = rec.data.key;
448 var entry = rows[key];
449
450 var rendered = me.renderKey(key, {}, rec);
451 var msg = Ext.String.format(warn, "'" + rendered + "'");
452
453 if (entry.del_extra_msg) {
454 msg += '<br>' + entry.del_extra_msg;
455 }
456
457 return msg;
458 },
459 handler: function(b, e, rec) {
460 Proxmox.Utils.API2Request({
461 url: '/api2/extjs/' + baseurl,
462 waitMsgTarget: me,
463 method: b.RESTMethod,
464 params: {
465 'delete': rec.data.key
466 },
467 callback: () => me.reload(),
468 failure: function (response, opts) {
469 Ext.Msg.alert('Error', response.htmlStatus);
470 },
471 success: function(response, options) {
472 if (b.RESTMethod === 'POST') {
473 var upid = response.result.data;
474 var win = Ext.create('Proxmox.window.TaskProgress', {
475 upid: upid,
476 listeners: {
477 destroy: () => me.reload(),
478 }
479 });
480 win.show();
481 }
482 }
483 });
484 },
485 listeners: {
486 render: function(btn) {
487 // hack: calculate an optimal button width on first display
488 // to prevent the whole toolbar to move when we switch
489 // between the "Remove" and "Detach" labels
490 var def = btn.getSize().width;
491
492 btn.setText(btn.altText);
493 var alt = btn.getSize().width;
494
495 btn.setText(btn.defaultText);
496
497 var optimal = alt > def ? alt : def;
498 btn.setSize({ width: optimal });
499 }
500 }
501 });
502
503 var revert_btn = new PVE.button.PendingRevert({
504 apiurl: '/api2/extjs/' + baseurl,
505 });
506
507 var efidisk_menuitem = Ext.create('Ext.menu.Item',{
508 text: gettext('EFI Disk'),
509 iconCls: 'fa fa-fw fa-hdd-o black',
510 disabled: !caps.vms['VM.Config.Disk'],
511 handler: function() {
512 let bios = me.rstore.getData().map.bios;
513 let usesEFI = bios && (bios.data.value === 'ovmf' || bios.data.pending === 'ovmf');
514
515 var win = Ext.create('PVE.qemu.EFIDiskEdit', {
516 url: '/api2/extjs/' + baseurl,
517 pveSelNode: me.pveSelNode,
518 usesEFI: usesEFI,
519 });
520 win.on('destroy', me.reload, me);
521 win.show();
522 }
523 });
524
525 var set_button_status = function() {
526 var sm = me.getSelectionModel();
527 var rec = sm.getSelection()[0];
528
529 // disable button when we have an efidisk already
530 // disable is ok in this case, because you can instantly
531 // see that there is already one
532 efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined);
533 // en/disable usb add button
534 var usbcount = 0;
535 var pcicount = 0;
536 var audiocount = 0;
537 var hasCloudInit = false;
538 me.rstore.getData().items.forEach(function(item){
539 if (/^usb\d+/.test(item.id)) {
540 usbcount++;
541 } else if (/^hostpci\d+/.test(item.id)) {
542 pcicount++;
543 } else if (/^audio\d+/.test(item.id)) {
544 audiocount++;
545 }
546 if (!hasCloudInit && /vm-.*-cloudinit/.test(item.data.value)) {
547 hasCloudInit = true;
548 }
549 });
550
551 // heuristic only for disabling some stuff, the backend has the final word.
552 var noSysConsolePerm = !caps.nodes['Sys.Console'];
553 var noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType'];
554
555 me.down('#addusb').setDisabled(noSysConsolePerm || (usbcount >= 5));
556 me.down('#addpci').setDisabled(noSysConsolePerm || (pcicount >= 4));
557 me.down('#addaudio').setDisabled(noVMConfigHWTypePerm || (audiocount >= 1));
558 me.down('#addci').setDisabled(noSysConsolePerm || hasCloudInit);
559
560 if (!rec) {
561 remove_btn.disable();
562 edit_btn.disable();
563 resize_btn.disable();
564 move_btn.disable();
565 revert_btn.disable();
566 return;
567 }
568 var key = rec.data.key;
569 var value = rec.data.value;
570 var rowdef = rows[key];
571
572 var pending = rec.data['delete'] || me.hasPendingChanges(key);
573 var isCDRom = (value && !!value.toString().match(/media=cdrom/));
574 var isUnusedDisk = key.match(/^unused\d+/);
575 var isUsedDisk = !isUnusedDisk && rowdef.isOnStorageBus && !isCDRom;
576
577 var isCloudInit = (value && value.toString().match(/vm-.*-cloudinit/));
578
579 var isEfi = (key === 'efidisk0');
580
581 remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true) || (isUnusedDisk && !diskCap));
582 remove_btn.setText((isUsedDisk && !isCloudInit) ? remove_btn.altText : remove_btn.defaultText);
583 remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT';
584
585 edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor || isCloudInit || (!isCDRom && !diskCap));
586
587 resize_btn.setDisabled(pending || !isUsedDisk || !diskCap);
588
589 move_btn.setDisabled(pending || !(isUsedDisk || isEfi) || !diskCap);
590
591 revert_btn.setDisabled(!pending);
592
593 };
594
595 Ext.apply(me, {
596 url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending',
597 interval: 5000,
598 selModel: sm,
599 run_editor: run_editor,
600 tbar: [
601 {
602 text: gettext('Add'),
603 menu: new Ext.menu.Menu({
604 items: [
605 {
606 text: gettext('Hard Disk'),
607 iconCls: 'fa fa-fw fa-hdd-o black',
608 disabled: !caps.vms['VM.Config.Disk'],
609 handler: function() {
610 var win = Ext.create('PVE.qemu.HDEdit', {
611 url: '/api2/extjs/' + baseurl,
612 pveSelNode: me.pveSelNode
613 });
614 win.on('destroy', me.reload, me);
615 win.show();
616 }
617 },
618 {
619 text: gettext('CD/DVD Drive'),
620 iconCls: 'pve-itype-icon-cdrom',
621 disabled: !caps.vms['VM.Config.Disk'],
622 handler: function() {
623 var win = Ext.create('PVE.qemu.CDEdit', {
624 url: '/api2/extjs/' + baseurl,
625 pveSelNode: me.pveSelNode
626 });
627 win.on('destroy', me.reload, me);
628 win.show();
629 }
630 },
631 {
632 text: gettext('Network Device'),
633 iconCls: 'fa fa-fw fa-exchange black',
634 disabled: !caps.vms['VM.Config.Network'],
635 handler: function() {
636 var win = Ext.create('PVE.qemu.NetworkEdit', {
637 url: '/api2/extjs/' + baseurl,
638 pveSelNode: me.pveSelNode,
639 isCreate: true
640 });
641 win.on('destroy', me.reload, me);
642 win.show();
643 }
644 },
645 efidisk_menuitem,
646 {
647 text: gettext('USB Device'),
648 itemId: 'addusb',
649 iconCls: 'fa fa-fw fa-usb black',
650 disabled: !caps.nodes['Sys.Console'],
651 handler: function() {
652 var win = Ext.create('PVE.qemu.USBEdit', {
653 url: '/api2/extjs/' + baseurl,
654 pveSelNode: me.pveSelNode
655 });
656 win.on('destroy', me.reload, me);
657 win.show();
658 }
659 },
660 {
661 text: gettext('PCI Device'),
662 itemId: 'addpci',
663 iconCls: 'pve-itype-icon-pci',
664 disabled: !caps.nodes['Sys.Console'],
665 handler: function() {
666 var win = Ext.create('PVE.qemu.PCIEdit', {
667 url: '/api2/extjs/' + baseurl,
668 pveSelNode: me.pveSelNode
669 });
670 win.on('destroy', me.reload, me);
671 win.show();
672 }
673 },
674 {
675 text: gettext('Serial Port'),
676 itemId: 'addserial',
677 iconCls: 'pve-itype-icon-serial',
678 disabled: !caps.vms['VM.Config.Options'],
679 handler: function() {
680 var win = Ext.create('PVE.qemu.SerialEdit', {
681 url: '/api2/extjs/' + baseurl
682 });
683 win.on('destroy', me.reload, me);
684 win.show();
685 }
686 },
687 {
688 text: gettext('CloudInit Drive'),
689 itemId: 'addci',
690 iconCls: 'fa fa-fw fa-cloud black',
691 disabled: !caps.nodes['Sys.Console'],
692 handler: function() {
693 var win = Ext.create('PVE.qemu.CIDriveEdit', {
694 url: '/api2/extjs/' + baseurl,
695 pveSelNode: me.pveSelNode
696 });
697 win.on('destroy', me.reload, me);
698 win.show();
699 }
700 },
701 {
702 text: gettext('Audio Device'),
703 itemId: 'addaudio',
704 iconCls: 'fa fa-fw fa-volume-up black',
705 disabled: !caps.vms['VM.Config.HWType'],
706 handler: function() {
707 var win = Ext.create('PVE.qemu.AudioEdit', {
708 url: '/api2/extjs/' + baseurl,
709 isCreate: true,
710 isAdd: true
711 });
712 win.on('destroy', me.reload, me);
713 win.show();
714 }
715 }
716 ]
717 })
718 },
719 remove_btn,
720 edit_btn,
721 resize_btn,
722 move_btn,
723 revert_btn
724 ],
725 rows: rows,
726 sorterFn: sorterFn,
727 listeners: {
728 itemdblclick: run_editor,
729 selectionchange: set_button_status
730 }
731 });
732
733 me.callParent();
734
735 me.on('activate', me.rstore.startUpdate);
736 me.on('destroy', me.rstore.stopUpdate);
737
738 me.mon(me.getStore(), 'datachanged', function() {
739 set_button_status();
740 });
741 }
742 });