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