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