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