]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/qemu/HardwareView.js
ui: vm/efi: always allow to add EFI disk even if OVMF is not yet set
[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 cores: {
189 visible: false
190 },
191 cpu: {
192 visible: false
193 },
194 numa: {
195 visible: false
196 },
197 balloon: {
198 visible: false
199 },
200 hotplug: {
201 visible: false
202 },
203 vcpus: {
204 visible: false
205 },
206 cpuunits: {
207 visible: false
208 },
209 cpulimit: {
210 visible: false
211 },
212 shares: {
213 visible: false
214 }
215 };
216 /*jslint confusion: false */
217
218 PVE.Utils.forEachBus(undefined, function(type, id) {
219 var confid = type + id;
220 rows[confid] = {
221 group: 10,
222 iconCls: 'hdd-o',
223 editor: 'PVE.qemu.HDEdit',
224 never_delete: caps.vms['VM.Config.Disk'] ? false : true,
225 isOnStorageBus: true,
226 header: gettext('Hard Disk') + ' (' + confid +')',
227 cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
228 cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')'
229 };
230 });
231 for (i = 0; i < 32; i++) {
232 confid = "net" + i.toString();
233 rows[confid] = {
234 group: 15,
235 order: i,
236 iconCls: 'exchange',
237 editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
238 never_delete: caps.vms['VM.Config.Network'] ? false : true,
239 header: gettext('Network Device') + ' (' + confid +')'
240 };
241 }
242 rows.efidisk0 = {
243 group: 20,
244 iconCls: 'hdd-o',
245 editor: null,
246 never_delete: caps.vms['VM.Config.Disk'] ? false : true,
247 header: gettext('EFI Disk')
248 };
249 for (i = 0; i < 5; i++) {
250 confid = "usb" + i.toString();
251 rows[confid] = {
252 group: 25,
253 order: i,
254 iconCls: 'usb',
255 editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined,
256 never_delete: caps.nodes['Sys.Console'] ? false : true,
257 header: gettext('USB Device') + ' (' + confid + ')'
258 };
259 }
260 for (i = 0; i < 4; i++) {
261 confid = "hostpci" + i.toString();
262 rows[confid] = {
263 group: 30,
264 order: i,
265 tdCls: 'pve-itype-icon-pci',
266 never_delete: caps.nodes['Sys.Console'] ? false : true,
267 editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined,
268 header: gettext('PCI Device') + ' (' + confid + ')'
269 };
270 }
271 for (i = 0; i < 4; i++) {
272 confid = "serial" + i.toString();
273 rows[confid] = {
274 group: 35,
275 order: i,
276 tdCls: 'pve-itype-icon-serial',
277 never_delete: caps.nodes['Sys.Console'] ? false : true,
278 header: gettext('Serial Port') + ' (' + confid + ')'
279 };
280 }
281 rows.audio0 = {
282 group: 40,
283 iconCls: 'volume-up',
284 editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined,
285 never_delete: caps.vms['VM.Config.HWType'] ? false : true,
286 header: gettext('Audio Device')
287 };
288 for (i = 0; i < 256; i++) {
289 rows["unused" + i.toString()] = {
290 group: 99,
291 order: i,
292 iconCls: 'hdd-o',
293 editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
294 header: gettext('Unused Disk') + ' ' + i.toString()
295 };
296 }
297
298 var sorterFn = function(rec1, rec2) {
299 var v1 = rec1.data.key;
300 var v2 = rec2.data.key;
301 var g1 = rows[v1].group || 0;
302 var g2 = rows[v2].group || 0;
303 var order1 = rows[v1].order || 0;
304 var order2 = rows[v2].order || 0;
305
306 if ((g1 - g2) !== 0) {
307 return g1 - g2;
308 }
309
310 if ((order1 - order2) !== 0) {
311 return order1 - order2;
312 }
313
314 if (v1 > v2) {
315 return 1;
316 } else if (v1 < v2) {
317 return -1;
318 } else {
319 return 0;
320 }
321 };
322
323 var reload = function() {
324 me.rstore.load();
325 };
326
327 var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
328
329 var sm = Ext.create('Ext.selection.RowModel', {});
330
331 var run_editor = function() {
332 var rec = sm.getSelection()[0];
333 if (!rec) {
334 return;
335 }
336
337 var rowdef = rows[rec.data.key];
338 if (!rowdef.editor) {
339 return;
340 }
341
342 var editor = rowdef.editor;
343 if (rowdef.isOnStorageBus) {
344 var value = me.getObjectValue(rec.data.key, '', true);
345 if (value.match(/vm-.*-cloudinit/)) {
346 return;
347 } else if (value.match(/media=cdrom/)) {
348 editor = 'PVE.qemu.CDEdit';
349 } else if (!diskCap) {
350 return;
351 }
352 }
353
354 var win;
355
356 if (Ext.isString(editor)) {
357 win = Ext.create(editor, {
358 pveSelNode: me.pveSelNode,
359 confid: rec.data.key,
360 url: '/api2/extjs/' + baseurl
361 });
362 } else {
363 var config = Ext.apply({
364 pveSelNode: me.pveSelNode,
365 confid: rec.data.key,
366 url: '/api2/extjs/' + baseurl
367 }, rowdef.editor);
368 win = Ext.createWidget(rowdef.editor.xtype, config);
369 win.load();
370 }
371
372 win.show();
373 win.on('destroy', reload);
374 };
375
376 var run_resize = function() {
377 var rec = sm.getSelection()[0];
378 if (!rec) {
379 return;
380 }
381
382 var win = Ext.create('PVE.window.HDResize', {
383 disk: rec.data.key,
384 nodename: nodename,
385 vmid: vmid
386 });
387
388 win.show();
389
390 win.on('destroy', reload);
391 };
392
393 var run_move = function() {
394 var rec = sm.getSelection()[0];
395 if (!rec) {
396 return;
397 }
398
399 var win = Ext.create('PVE.window.HDMove', {
400 disk: rec.data.key,
401 nodename: nodename,
402 vmid: vmid
403 });
404
405 win.show();
406
407 win.on('destroy', reload);
408 };
409
410 var edit_btn = new Proxmox.button.Button({
411 text: gettext('Edit'),
412 selModel: sm,
413 disabled: true,
414 handler: run_editor
415 });
416
417 var resize_btn = new Proxmox.button.Button({
418 text: gettext('Resize disk'),
419 selModel: sm,
420 disabled: true,
421 handler: run_resize
422 });
423
424 var move_btn = new Proxmox.button.Button({
425 text: gettext('Move disk'),
426 selModel: sm,
427 disabled: true,
428 handler: run_move
429 });
430
431 var remove_btn = new Proxmox.button.Button({
432 text: gettext('Remove'),
433 defaultText: gettext('Remove'),
434 altText: gettext('Detach'),
435 selModel: sm,
436 disabled: true,
437 dangerous: true,
438 RESTMethod: 'PUT',
439 confirmMsg: function(rec) {
440 var warn = gettext('Are you sure you want to remove entry {0}');
441 if (this.text === this.altText) {
442 warn = gettext('Are you sure you want to detach entry {0}');
443 }
444
445 var entry = rec.data.key;
446 var rendered = me.renderKey(entry, {}, rec);
447 var msg = Ext.String.format(warn, "'" + rendered + "'");
448
449 if (entry.match(/^unused\d+$/)) {
450 msg += " " + gettext('This will permanently erase all data.');
451 }
452
453 return msg;
454 },
455 handler: function(b, e, rec) {
456 Proxmox.Utils.API2Request({
457 url: '/api2/extjs/' + baseurl,
458 waitMsgTarget: me,
459 method: b.RESTMethod,
460 params: {
461 'delete': rec.data.key
462 },
463 callback: function() {
464 reload();
465 },
466 failure: function (response, opts) {
467 Ext.Msg.alert('Error', response.htmlStatus);
468 },
469 success: function(response, options) {
470 if (b.RESTMethod === 'POST') {
471 var upid = response.result.data;
472 var win = Ext.create('Proxmox.window.TaskProgress', {
473 upid: upid,
474 listeners: {
475 destroy: function () {
476 me.reload();
477 }
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 Proxmox.button.Button({
504 text: gettext('Revert'),
505 selModel: sm,
506 disabled: true,
507 handler: function(b, e, rec) {
508 var rowdef = me.rows[rec.data.key] || {};
509 var keys = rowdef.multiKey || [ rec.data.key ];
510 var revert = keys.join(',');
511 Proxmox.Utils.API2Request({
512 url: '/api2/extjs/' + baseurl,
513 waitMsgTarget: me,
514 method: 'PUT',
515 params: {
516 'revert': revert
517 },
518 callback: function() {
519 reload();
520 },
521 failure: function (response, opts) {
522 Ext.Msg.alert('Error',response.htmlStatus);
523 }
524 });
525 }
526 });
527
528 var efidisk_menuitem = Ext.create('Ext.menu.Item',{
529 text: gettext('EFI Disk'),
530 iconCls: 'fa fa-fw fa-hdd-o black',
531 disabled: !caps.vms['VM.Config.Disk'],
532 handler: function() {
533 let bios = me.rstore.getData().map.bios;
534 let usesEFI = bios && (bios.data.value === 'ovmf' || bios.data.pending === 'ovmf');
535
536 var win = Ext.create('PVE.qemu.EFIDiskEdit', {
537 url: '/api2/extjs/' + baseurl,
538 pveSelNode: me.pveSelNode,
539 usesEFI: usesEFI,
540 });
541 win.on('destroy', reload);
542 win.show();
543 }
544 });
545
546 var set_button_status = function() {
547 var sm = me.getSelectionModel();
548 var rec = sm.getSelection()[0];
549
550 // disable button when we have an efidisk already
551 // disable is ok in this case, because you can instantly
552 // see that there is already one
553 efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined);
554 // en/disable usb add button
555 var usbcount = 0;
556 var pcicount = 0;
557 var audiocount = 0;
558 var hasCloudInit = false;
559 me.rstore.getData().items.forEach(function(item){
560 if (/^usb\d+/.test(item.id)) {
561 usbcount++;
562 } else if (/^hostpci\d+/.test(item.id)) {
563 pcicount++;
564 } else if (/^audio\d+/.test(item.id)) {
565 audiocount++;
566 }
567 if (!hasCloudInit && /vm-.*-cloudinit/.test(item.data.value)) {
568 hasCloudInit = true;
569 }
570 });
571
572 // heuristic only for disabling some stuff, the backend has the final word.
573 var noSysConsolePerm = !caps.nodes['Sys.Console'];
574 var noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType'];
575
576 me.down('#addusb').setDisabled(noSysConsolePerm || (usbcount >= 5));
577 me.down('#addpci').setDisabled(noSysConsolePerm || (pcicount >= 4));
578 me.down('#addaudio').setDisabled(noVMConfigHWTypePerm || (audiocount >= 1));
579 me.down('#addci').setDisabled(noSysConsolePerm || hasCloudInit);
580
581 if (!rec) {
582 remove_btn.disable();
583 edit_btn.disable();
584 resize_btn.disable();
585 move_btn.disable();
586 revert_btn.disable();
587 return;
588 }
589 var key = rec.data.key;
590 var value = rec.data.value;
591 var rowdef = rows[key];
592
593 var pending = rec.data['delete'] || me.hasPendingChanges(key);
594 var isCDRom = (value && !!value.toString().match(/media=cdrom/));
595 var isUnusedDisk = key.match(/^unused\d+/);
596 var isUsedDisk = !isUnusedDisk && rowdef.isOnStorageBus && !isCDRom;
597
598 var isCloudInit = (value && value.toString().match(/vm-.*-cloudinit/));
599
600 var isEfi = (key === 'efidisk0');
601
602 remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true) || (isUnusedDisk && !diskCap));
603 remove_btn.setText((isUsedDisk && !isCloudInit) ? remove_btn.altText : remove_btn.defaultText);
604 remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT';
605
606 edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor || isCloudInit || (!isCDRom && !diskCap));
607
608 resize_btn.setDisabled(pending || !isUsedDisk || !diskCap);
609
610 move_btn.setDisabled(pending || !isUsedDisk || !diskCap);
611
612 revert_btn.setDisabled(!pending);
613
614 };
615
616 Ext.apply(me, {
617 url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending',
618 interval: 5000,
619 selModel: sm,
620 run_editor: run_editor,
621 tbar: [
622 {
623 text: gettext('Add'),
624 menu: new Ext.menu.Menu({
625 items: [
626 {
627 text: gettext('Hard Disk'),
628 iconCls: 'fa fa-fw fa-hdd-o black',
629 disabled: !caps.vms['VM.Config.Disk'],
630 handler: function() {
631 var win = Ext.create('PVE.qemu.HDEdit', {
632 url: '/api2/extjs/' + baseurl,
633 pveSelNode: me.pveSelNode
634 });
635 win.on('destroy', reload);
636 win.show();
637 }
638 },
639 {
640 text: gettext('CD/DVD Drive'),
641 iconCls: 'pve-itype-icon-cdrom',
642 disabled: !caps.vms['VM.Config.Disk'],
643 handler: function() {
644 var win = Ext.create('PVE.qemu.CDEdit', {
645 url: '/api2/extjs/' + baseurl,
646 pveSelNode: me.pveSelNode
647 });
648 win.on('destroy', reload);
649 win.show();
650 }
651 },
652 {
653 text: gettext('Network Device'),
654 iconCls: 'fa fa-fw fa-exchange black',
655 disabled: !caps.vms['VM.Config.Network'],
656 handler: function() {
657 var win = Ext.create('PVE.qemu.NetworkEdit', {
658 url: '/api2/extjs/' + baseurl,
659 pveSelNode: me.pveSelNode,
660 isCreate: true
661 });
662 win.on('destroy', reload);
663 win.show();
664 }
665 },
666 efidisk_menuitem,
667 {
668 text: gettext('USB Device'),
669 itemId: 'addusb',
670 iconCls: 'fa fa-fw fa-usb black',
671 disabled: !caps.nodes['Sys.Console'],
672 handler: function() {
673 var win = Ext.create('PVE.qemu.USBEdit', {
674 url: '/api2/extjs/' + baseurl,
675 pveSelNode: me.pveSelNode
676 });
677 win.on('destroy', reload);
678 win.show();
679 }
680 },
681 {
682 text: gettext('PCI Device'),
683 itemId: 'addpci',
684 iconCls: 'pve-itype-icon-pci',
685 disabled: !caps.nodes['Sys.Console'],
686 handler: function() {
687 var win = Ext.create('PVE.qemu.PCIEdit', {
688 url: '/api2/extjs/' + baseurl,
689 pveSelNode: me.pveSelNode
690 });
691 win.on('destroy', reload);
692 win.show();
693 }
694 },
695 {
696 text: gettext('Serial Port'),
697 itemId: 'addserial',
698 iconCls: 'pve-itype-icon-serial',
699 disabled: !caps.vms['VM.Config.Options'],
700 handler: function() {
701 var win = Ext.create('PVE.qemu.SerialEdit', {
702 url: '/api2/extjs/' + baseurl
703 });
704 win.on('destroy', reload);
705 win.show();
706 }
707 },
708 {
709 text: gettext('CloudInit Drive'),
710 itemId: 'addci',
711 iconCls: 'fa fa-fw fa-cloud black',
712 disabled: !caps.nodes['Sys.Console'],
713 handler: function() {
714 var win = Ext.create('PVE.qemu.CIDriveEdit', {
715 url: '/api2/extjs/' + baseurl,
716 pveSelNode: me.pveSelNode
717 });
718 win.on('destroy', reload);
719 win.show();
720 }
721 },
722 {
723 text: gettext('Audio Device'),
724 itemId: 'addaudio',
725 iconCls: 'fa fa-fw fa-volume-up black',
726 disabled: !caps.vms['VM.Config.HWType'],
727 handler: function() {
728 var win = Ext.create('PVE.qemu.AudioEdit', {
729 url: '/api2/extjs/' + baseurl,
730 isCreate: true,
731 isAdd: true
732 });
733 win.on('destroy', reload);
734 win.show();
735 }
736 }
737 ]
738 })
739 },
740 remove_btn,
741 edit_btn,
742 resize_btn,
743 move_btn,
744 revert_btn
745 ],
746 rows: rows,
747 sorterFn: sorterFn,
748 listeners: {
749 itemdblclick: run_editor,
750 selectionchange: set_button_status
751 }
752 });
753
754 me.callParent();
755
756 me.on('activate', me.rstore.startUpdate);
757 me.on('destroy', me.rstore.stopUpdate);
758
759 me.mon(me.rstore, 'refresh', function() {
760 set_button_status();
761 });
762 }
763 });