]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/qemu/HardwareView.js
ui: pending revert: rename baseurl config to apiurl
[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 PVE.button.PendingRevert({
504 pendingGrid: me,
505 apiurl: '/api2/extjs/' + baseurl,
506 });
507
508 var efidisk_menuitem = Ext.create('Ext.menu.Item',{
509 text: gettext('EFI Disk'),
510 iconCls: 'fa fa-fw fa-hdd-o black',
511 disabled: !caps.vms['VM.Config.Disk'],
512 handler: function() {
513 let bios = me.rstore.getData().map.bios;
514 let usesEFI = bios && (bios.data.value === 'ovmf' || bios.data.pending === 'ovmf');
515
516 var win = Ext.create('PVE.qemu.EFIDiskEdit', {
517 url: '/api2/extjs/' + baseurl,
518 pveSelNode: me.pveSelNode,
519 usesEFI: usesEFI,
520 });
521 win.on('destroy', reload);
522 win.show();
523 }
524 });
525
526 var set_button_status = function() {
527 var sm = me.getSelectionModel();
528 var rec = sm.getSelection()[0];
529
530 // disable button when we have an efidisk already
531 // disable is ok in this case, because you can instantly
532 // see that there is already one
533 efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined);
534 // en/disable usb add button
535 var usbcount = 0;
536 var pcicount = 0;
537 var audiocount = 0;
538 var hasCloudInit = false;
539 me.rstore.getData().items.forEach(function(item){
540 if (/^usb\d+/.test(item.id)) {
541 usbcount++;
542 } else if (/^hostpci\d+/.test(item.id)) {
543 pcicount++;
544 } else if (/^audio\d+/.test(item.id)) {
545 audiocount++;
546 }
547 if (!hasCloudInit && /vm-.*-cloudinit/.test(item.data.value)) {
548 hasCloudInit = true;
549 }
550 });
551
552 // heuristic only for disabling some stuff, the backend has the final word.
553 var noSysConsolePerm = !caps.nodes['Sys.Console'];
554 var noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType'];
555
556 me.down('#addusb').setDisabled(noSysConsolePerm || (usbcount >= 5));
557 me.down('#addpci').setDisabled(noSysConsolePerm || (pcicount >= 4));
558 me.down('#addaudio').setDisabled(noVMConfigHWTypePerm || (audiocount >= 1));
559 me.down('#addci').setDisabled(noSysConsolePerm || hasCloudInit);
560
561 if (!rec) {
562 remove_btn.disable();
563 edit_btn.disable();
564 resize_btn.disable();
565 move_btn.disable();
566 revert_btn.disable();
567 return;
568 }
569 var key = rec.data.key;
570 var value = rec.data.value;
571 var rowdef = rows[key];
572
573 var pending = rec.data['delete'] || me.hasPendingChanges(key);
574 var isCDRom = (value && !!value.toString().match(/media=cdrom/));
575 var isUnusedDisk = key.match(/^unused\d+/);
576 var isUsedDisk = !isUnusedDisk && rowdef.isOnStorageBus && !isCDRom;
577
578 var isCloudInit = (value && value.toString().match(/vm-.*-cloudinit/));
579
580 var isEfi = (key === 'efidisk0');
581
582 remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true) || (isUnusedDisk && !diskCap));
583 remove_btn.setText((isUsedDisk && !isCloudInit) ? remove_btn.altText : remove_btn.defaultText);
584 remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT';
585
586 edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor || isCloudInit || (!isCDRom && !diskCap));
587
588 resize_btn.setDisabled(pending || !isUsedDisk || !diskCap);
589
590 move_btn.setDisabled(pending || !(isUsedDisk || isEfi) || !diskCap);
591
592 revert_btn.setDisabled(!pending);
593
594 };
595
596 Ext.apply(me, {
597 url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending',
598 interval: 5000,
599 selModel: sm,
600 run_editor: run_editor,
601 tbar: [
602 {
603 text: gettext('Add'),
604 menu: new Ext.menu.Menu({
605 items: [
606 {
607 text: gettext('Hard Disk'),
608 iconCls: 'fa fa-fw fa-hdd-o black',
609 disabled: !caps.vms['VM.Config.Disk'],
610 handler: function() {
611 var win = Ext.create('PVE.qemu.HDEdit', {
612 url: '/api2/extjs/' + baseurl,
613 pveSelNode: me.pveSelNode
614 });
615 win.on('destroy', reload);
616 win.show();
617 }
618 },
619 {
620 text: gettext('CD/DVD Drive'),
621 iconCls: 'pve-itype-icon-cdrom',
622 disabled: !caps.vms['VM.Config.Disk'],
623 handler: function() {
624 var win = Ext.create('PVE.qemu.CDEdit', {
625 url: '/api2/extjs/' + baseurl,
626 pveSelNode: me.pveSelNode
627 });
628 win.on('destroy', reload);
629 win.show();
630 }
631 },
632 {
633 text: gettext('Network Device'),
634 iconCls: 'fa fa-fw fa-exchange black',
635 disabled: !caps.vms['VM.Config.Network'],
636 handler: function() {
637 var win = Ext.create('PVE.qemu.NetworkEdit', {
638 url: '/api2/extjs/' + baseurl,
639 pveSelNode: me.pveSelNode,
640 isCreate: true
641 });
642 win.on('destroy', reload);
643 win.show();
644 }
645 },
646 efidisk_menuitem,
647 {
648 text: gettext('USB Device'),
649 itemId: 'addusb',
650 iconCls: 'fa fa-fw fa-usb black',
651 disabled: !caps.nodes['Sys.Console'],
652 handler: function() {
653 var win = Ext.create('PVE.qemu.USBEdit', {
654 url: '/api2/extjs/' + baseurl,
655 pveSelNode: me.pveSelNode
656 });
657 win.on('destroy', reload);
658 win.show();
659 }
660 },
661 {
662 text: gettext('PCI Device'),
663 itemId: 'addpci',
664 iconCls: 'pve-itype-icon-pci',
665 disabled: !caps.nodes['Sys.Console'],
666 handler: function() {
667 var win = Ext.create('PVE.qemu.PCIEdit', {
668 url: '/api2/extjs/' + baseurl,
669 pveSelNode: me.pveSelNode
670 });
671 win.on('destroy', reload);
672 win.show();
673 }
674 },
675 {
676 text: gettext('Serial Port'),
677 itemId: 'addserial',
678 iconCls: 'pve-itype-icon-serial',
679 disabled: !caps.vms['VM.Config.Options'],
680 handler: function() {
681 var win = Ext.create('PVE.qemu.SerialEdit', {
682 url: '/api2/extjs/' + baseurl
683 });
684 win.on('destroy', reload);
685 win.show();
686 }
687 },
688 {
689 text: gettext('CloudInit Drive'),
690 itemId: 'addci',
691 iconCls: 'fa fa-fw fa-cloud black',
692 disabled: !caps.nodes['Sys.Console'],
693 handler: function() {
694 var win = Ext.create('PVE.qemu.CIDriveEdit', {
695 url: '/api2/extjs/' + baseurl,
696 pveSelNode: me.pveSelNode
697 });
698 win.on('destroy', reload);
699 win.show();
700 }
701 },
702 {
703 text: gettext('Audio Device'),
704 itemId: 'addaudio',
705 iconCls: 'fa fa-fw fa-volume-up black',
706 disabled: !caps.vms['VM.Config.HWType'],
707 handler: function() {
708 var win = Ext.create('PVE.qemu.AudioEdit', {
709 url: '/api2/extjs/' + baseurl,
710 isCreate: true,
711 isAdd: true
712 });
713 win.on('destroy', reload);
714 win.show();
715 }
716 }
717 ]
718 })
719 },
720 remove_btn,
721 edit_btn,
722 resize_btn,
723 move_btn,
724 revert_btn
725 ],
726 rows: rows,
727 sorterFn: sorterFn,
728 listeners: {
729 itemdblclick: run_editor,
730 selectionchange: set_button_status
731 }
732 });
733
734 me.callParent();
735
736 me.on('activate', me.rstore.startUpdate);
737 me.on('destroy', me.rstore.stopUpdate);
738
739 me.mon(me.getStore(), 'datachanged', function() {
740 set_button_status();
741 });
742 }
743 });