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