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