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