]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/qemu/HardwareView.js
update shipped appliance info index
[pve-manager.git] / www / manager6 / qemu / HardwareView.js
CommitLineData
f97670f5 1Ext.define('PVE.qemu.HardwareView', {
bc0eaf58 2 extend: 'Proxmox.grid.PendingObjectGrid',
f97670f5
DM
3 alias: ['widget.PVE.qemu.HardwareView'],
4
ba93a9c6
DC
5 onlineHelp: 'qm_virtual_machines_settings',
6
f97670f5
DM
7 renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
8 var me = this;
9 var rows = me.rows;
10 var rowdef = rows[key] || {};
6a040799
DC
11 var iconCls = rowdef.iconCls;
12 var icon = '';
53e3ea84 13 var txt = rowdef.header || key;
f97670f5
DM
14
15 metaData.tdAttr = "valign=middle";
16
0ea3b849
TL
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
f97670f5
DM
31 if (rowdef.tdCls) {
32 metaData.tdCls = rowdef.tdCls;
6a040799
DC
33 } else if (iconCls) {
34 icon = "<i class='pve-grid-fa fa fa-fw fa-" + iconCls + "'></i>";
35 metaData.tdCls += " pve-itype-fa";
f97670f5 36 }
03558174
TL
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 }
f97670f5
DM
44 },
45
8058410f 46 initComponent: function() {
f97670f5 47 var me = this;
f97670f5 48
b49a0404 49 const { node: nodename, vmid } = me.pveSelNode.data;
2a4971d8 50 if (!nodename) {
f97670f5 51 throw "no node name specified";
b49a0404 52 } else if (!vmid) {
f97670f5
DM
53 throw "no VM ID specified";
54 }
55
96da47ba
TL
56 const caps = Ext.state.Manager.get('GuiCap');
57 const diskCap = caps.vms['VM.Config.Disk'];
58 const cdromCap = caps.vms['VM.Config.CDROM'];
f97670f5 59
b49a0404
TL
60 let isCloudInitKey = v => v && v.toString().match(/vm-.*-cloudinit/);
61
5a2e333c
FE
62 const nodeInfo = PVE.data.ResourceStore.getNodes().find(node => node.node === nodename);
63 let processorEditor = {
64 xtype: 'pveQemuProcessorEdit',
65 cgroupMode: nodeInfo['cgroup-mode'],
66 };
67
96da47ba 68 let rows = {
f97670f5
DM
69 memory: {
70 header: gettext('Memory'),
71 editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
72 never_delete: true,
72408db9 73 defaultValue: '512',
64bef820 74 tdCls: 'pve-itype-icon-memory',
cbe0b4b8 75 group: 2,
1d902252
DC
76 multiKey: ['memory', 'balloon', 'shares'],
77 renderer: function(value, metaData, record, ri, ci, store, pending) {
78 var res = '';
79
80 var max = me.getObjectValue('memory', 512, pending);
8058410f 81 var balloon = me.getObjectValue('balloon', undefined, pending);
1d902252
DC
82 var shares = me.getObjectValue('shares', undefined, pending);
83
8058410f 84 res = Proxmox.Utils.format_size(max*1024*1024);
1d902252
DC
85
86 if (balloon !== undefined && balloon > 0) {
87 res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res;
88
89 if (shares) {
90 res += ' [shares=' + shares +']';
91 }
92 } else if (balloon === 0) {
93 res += ' [balloon=0]';
94 }
95 return res;
f6710aac 96 },
f97670f5
DM
97 },
98 sockets: {
99 header: gettext('Processors'),
100 never_delete: true,
53e3ea84 101 editor: caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']
5a2e333c 102 ? processorEditor : undefined,
f9698f62 103 tdCls: 'pve-itype-icon-cpu',
cbe0b4b8 104 group: 3,
72408db9 105 defaultValue: '1',
f97670f5
DM
106 multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'],
107 renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
f97670f5
DM
108 var sockets = me.getObjectValue('sockets', 1, pending);
109 var model = me.getObjectValue('cpu', undefined, pending);
110 var cores = me.getObjectValue('cores', 1, pending);
111 var numa = me.getObjectValue('numa', undefined, pending);
112 var vcpus = me.getObjectValue('vcpus', undefined, pending);
113 var cpulimit = me.getObjectValue('cpulimit', undefined, pending);
114 var cpuunits = me.getObjectValue('cpuunits', undefined, pending);
115
b49a0404
TL
116 let res = Ext.String.format(
117 '{0} ({1} sockets, {2} cores)', sockets * cores, sockets, cores);
72408db9 118
f97670f5
DM
119 if (model) {
120 res += ' [' + model + ']';
121 }
f97670f5
DM
122 if (numa) {
123 res += ' [numa=' + numa +']';
124 }
f97670f5
DM
125 if (vcpus) {
126 res += ' [vcpus=' + vcpus +']';
127 }
f97670f5
DM
128 if (cpulimit) {
129 res += ' [cpulimit=' + cpulimit +']';
130 }
f97670f5
DM
131 if (cpuunits) {
132 res += ' [cpuunits=' + cpuunits +']';
133 }
134
135 return res;
f6710aac 136 },
f97670f5 137 },
439d7821
DC
138 bios: {
139 header: 'BIOS',
140 group: 4,
141 never_delete: true,
142 editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
143 defaultValue: '',
144 iconCls: 'microchip',
f6710aac 145 renderer: PVE.Utils.render_qemu_bios,
439d7821 146 },
f97670f5
DM
147 vga: {
148 header: gettext('Display'),
149 editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
150 never_delete: true,
a032a91a 151 iconCls: 'desktop',
8058410f 152 group: 5,
f97670f5 153 defaultValue: '',
f6710aac 154 renderer: PVE.Utils.render_kvm_vga_driver,
f97670f5 155 },
aae2273c
DC
156 machine: {
157 header: gettext('Machine'),
9dbddec7 158 editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.MachineEdit' : undefined,
aae2273c
DC
159 iconCls: 'cogs',
160 never_delete: true,
161 group: 6,
162 defaultValue: '',
4a0ccbad
SR
163 renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
164 let ostype = me.getObjectValue('ostype', undefined, pending);
165 if (PVE.Utils.is_windows(ostype) &&
166 (!value || value === 'pc' || value === 'q35')) {
167 return value === 'q35' ? 'pc-q35-5.1' : 'pc-i440fx-5.1';
168 }
169 return PVE.Utils.render_qemu_machine(value);
170 },
aae2273c 171 },
439d7821
DC
172 scsihw: {
173 header: gettext('SCSI Controller'),
174 iconCls: 'database',
175 editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined,
176 renderer: PVE.Utils.render_scsihw,
177 group: 7,
178 never_delete: true,
f6710aac 179 defaultValue: '',
439d7821 180 },
23dfddc6 181 vmstate: {
4b07ffdd 182 header: gettext('Hibernation VM State'),
23dfddc6 183 iconCls: 'download',
59bce287
TL
184 del_extra_msg: gettext('The saved VM state will be permanently lost.'),
185 group: 100,
23dfddc6 186 },
f97670f5 187 cores: {
f6710aac 188 visible: false,
f97670f5
DM
189 },
190 cpu: {
f6710aac 191 visible: false,
f97670f5
DM
192 },
193 numa: {
f6710aac 194 visible: false,
f97670f5
DM
195 },
196 balloon: {
f6710aac 197 visible: false,
f97670f5
DM
198 },
199 hotplug: {
f6710aac 200 visible: false,
f97670f5
DM
201 },
202 vcpus: {
f6710aac 203 visible: false,
f97670f5
DM
204 },
205 cpuunits: {
f6710aac 206 visible: false,
f97670f5
DM
207 },
208 cpulimit: {
f6710aac 209 visible: false,
3b37ab6d 210 },
1d902252 211 shares: {
f6710aac
TL
212 visible: false,
213 },
4a0ccbad
SR
214 ostype: {
215 visible: false,
216 },
f97670f5
DM
217 };
218
abe824aa 219 PVE.Utils.forEachBus(undefined, function(type, id) {
f8205155 220 let confid = type + id;
f97670f5 221 rows[confid] = {
8d554444 222 group: 10,
a032a91a 223 iconCls: 'hdd-o',
f97670f5 224 editor: 'PVE.qemu.HDEdit',
0ea3b849 225 isOnStorageBus: true,
f97670f5 226 header: gettext('Hard Disk') + ' (' + confid +')',
b38d0810 227 cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
f6710aac 228 cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')',
f97670f5 229 };
abe824aa 230 });
f8205155
AL
231 for (let i = 0; i < PVE.Utils.hardware_counts.net; i++) {
232 let confid = "net" + i.toString();
f97670f5 233 rows[confid] = {
8d554444 234 group: 15,
cbe0b4b8 235 order: i,
a032a91a 236 iconCls: 'exchange',
f97670f5 237 editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
ef725143 238 never_delete: !caps.vms['VM.Config.Network'],
f6710aac 239 header: gettext('Network Device') + ' (' + confid +')',
f97670f5
DM
240 };
241 }
194c9b09 242 rows.efidisk0 = {
8d554444 243 group: 20,
a032a91a 244 iconCls: 'hdd-o',
194c9b09 245 editor: null,
ef725143 246 never_delete: !caps.vms['VM.Config.Disk'],
f6710aac 247 header: gettext('EFI Disk'),
194c9b09 248 };
6d084964
SR
249 rows.tpmstate0 = {
250 group: 22,
251 iconCls: 'hdd-o',
252 editor: null,
253 never_delete: !caps.vms['VM.Config.Disk'],
254 header: gettext('TPM State'),
255 };
f8205155
AL
256 for (let i = 0; i < PVE.Utils.hardware_counts.usb; i++) {
257 let confid = "usb" + i.toString();
fa522e48 258 rows[confid] = {
8d554444 259 group: 25,
cbe0b4b8 260 order: i,
fa9cdaa1 261 iconCls: 'usb',
7becf34f
DC
262 editor: caps.nodes['Sys.Console'] || caps.mapping['Mapping.Use'] ? 'PVE.qemu.USBEdit' : undefined,
263 never_delete: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
f6710aac 264 header: gettext('USB Device') + ' (' + confid + ')',
fa522e48
DC
265 };
266 }
f8205155
AL
267 for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
268 let confid = "hostpci" + i.toString();
ec3d8372 269 rows[confid] = {
8d554444 270 group: 30,
cbe0b4b8 271 order: i,
ec3d8372 272 tdCls: 'pve-itype-icon-pci',
7becf34f
DC
273 never_delete: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
274 editor: caps.nodes['Sys.Console'] || caps.mapping['Mapping.Use'] ? 'PVE.qemu.PCIEdit' : undefined,
f6710aac 275 header: gettext('PCI Device') + ' (' + confid + ')',
ec3d8372
DC
276 };
277 }
f8205155
AL
278 for (let i = 0; i < PVE.Utils.hardware_counts.serial; i++) {
279 let confid = "serial" + i.toString();
a488da8c 280 rows[confid] = {
8d554444 281 group: 35,
cbe0b4b8 282 order: i,
a488da8c 283 tdCls: 'pve-itype-icon-serial',
ef725143 284 never_delete: !caps.nodes['Sys.Console'],
f6710aac 285 header: gettext('Serial Port') + ' (' + confid + ')',
a488da8c
DC
286 };
287 }
171ca270
AL
288 rows.audio0 = {
289 group: 40,
290 iconCls: 'volume-up',
291 editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined,
ef725143 292 never_delete: !caps.vms['VM.Config.HWType'],
f6710aac 293 header: gettext('Audio Device'),
171ca270 294 };
f8205155 295 for (let i = 0; i < 256; i++) {
72408db9 296 rows["unused" + i.toString()] = {
cd86ab24 297 group: 99,
cbe0b4b8 298 order: i,
0ea3b849 299 iconCls: 'hdd-o',
b2a5aa18 300 del_extra_msg: gettext('This will permanently erase all data.'),
f97670f5 301 editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
f6710aac 302 header: gettext('Unused Disk') + ' ' + i.toString(),
f97670f5
DM
303 };
304 }
6c1d8ead
SR
305 rows.rng0 = {
306 group: 45,
4ba4c7c6 307 tdCls: 'pve-itype-icon-die',
6c1d8ead 308 editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.RNGEdit' : undefined,
ef725143 309 never_delete: !caps.nodes['Sys.Console'],
f6710aac 310 header: gettext("VirtIO RNG"),
6c1d8ead 311 };
f97670f5
DM
312
313 var sorterFn = function(rec1, rec2) {
314 var v1 = rec1.data.key;
315 var v2 = rec2.data.key;
316 var g1 = rows[v1].group || 0;
317 var g2 = rows[v2].group || 0;
cbe0b4b8
DL
318 var order1 = rows[v1].order || 0;
319 var order2 = rows[v2].order || 0;
320
53e3ea84 321 if (g1 - g2 !== 0) {
cbe0b4b8
DL
322 return g1 - g2;
323 }
2a4971d8 324
53e3ea84 325 if (order1 - order2 !== 0) {
cbe0b4b8
DL
326 return order1 - order2;
327 }
328
329 if (v1 > v2) {
330 return 1;
331 } else if (v1 < v2) {
332 return -1;
333 } else {
334 return 0;
335 }
f97670f5
DM
336 };
337
b49a0404 338 let baseurl = `nodes/${nodename}/qemu/${vmid}/config`;
f97670f5 339
b49a0404 340 let sm = Ext.create('Ext.selection.RowModel', {});
f97670f5 341
b49a0404
TL
342 let run_editor = function() {
343 let rec = sm.getSelection()[0];
344 if (!rec || !rows[rec.data.key]?.editor) {
f97670f5
DM
345 return;
346 }
b49a0404
TL
347 let rowdef = rows[rec.data.key];
348 let editor = rowdef.editor;
f97670f5 349
0ea3b849 350 if (rowdef.isOnStorageBus) {
b49a0404
TL
351 let value = me.getObjectValue(rec.data.key, '', true);
352 if (isCloudInitKey(value)) {
ab1ee352
DC
353 return;
354 } else if (value.match(/media=cdrom/)) {
f97670f5 355 editor = 'PVE.qemu.CDEdit';
10525ed7
TL
356 } else if (!diskCap) {
357 return;
f97670f5
DM
358 }
359 }
360
b49a0404
TL
361 let commonOpts = {
362 autoShow: true,
363 pveSelNode: me.pveSelNode,
364 confid: rec.data.key,
365 url: `/api2/extjs/${baseurl}`,
366 listeners: {
367 destroy: () => me.reload(),
368 },
369 };
f97670f5
DM
370
371 if (Ext.isString(editor)) {
b49a0404 372 Ext.create(editor, commonOpts);
f97670f5 373 } else {
b49a0404 374 let win = Ext.createWidget(rowdef.editor.xtype, Ext.apply(commonOpts, rowdef.editor));
f97670f5
DM
375 win.load();
376 }
f97670f5
DM
377 };
378
b49a0404 379 let edit_btn = new Proxmox.button.Button({
f97670f5
DM
380 text: gettext('Edit'),
381 selModel: sm,
382 disabled: true,
f6710aac 383 handler: run_editor,
b49a0404 384 });
f97670f5 385
a8d854af
AL
386 let move_menuitem = new Ext.menu.Item({
387 text: gettext('Move Storage'),
388 tooltip: gettext('Move disk to another storage'),
389 iconCls: 'fa fa-database',
f97670f5 390 selModel: sm,
b49a0404
TL
391 handler: () => {
392 let rec = sm.getSelection()[0];
393 if (!rec) {
394 return;
395 }
a8d854af 396 Ext.create('PVE.window.HDMove', {
b49a0404
TL
397 autoShow: true,
398 disk: rec.data.key,
399 nodename: nodename,
400 vmid: vmid,
1b42be84 401 type: 'qemu',
b49a0404
TL
402 listeners: {
403 destroy: () => me.reload(),
404 },
405 });
406 },
f97670f5
DM
407 });
408
a8d854af
AL
409 let reassign_menuitem = new Ext.menu.Item({
410 text: gettext('Reassign Owner'),
411 tooltip: gettext('Reassign disk to another VM'),
412 iconCls: 'fa fa-desktop',
f97670f5 413 selModel: sm,
b49a0404 414 handler: () => {
a8d854af 415 let rec = sm.getSelection()[0];
b49a0404
TL
416 if (!rec) {
417 return;
418 }
a8d854af 419
3bde324f 420 Ext.create('PVE.window.GuestDiskReassign', {
b49a0404
TL
421 autoShow: true,
422 disk: rec.data.key,
423 nodename: nodename,
424 vmid: vmid,
a8d854af 425 type: 'qemu',
b49a0404
TL
426 listeners: {
427 destroy: () => me.reload(),
428 },
429 });
430 },
f97670f5
DM
431 });
432
a8d854af
AL
433 let resize_menuitem = new Ext.menu.Item({
434 text: gettext('Resize'),
435 iconCls: 'fa fa-plus',
436 selModel: sm,
437 handler: () => {
438 let rec = sm.getSelection()[0];
439 if (!rec) {
440 return;
441 }
442 Ext.create('PVE.window.HDResize', {
443 autoShow: true,
444 disk: rec.data.key,
445 nodename: nodename,
446 vmid: vmid,
447 listeners: {
448 destroy: () => me.reload(),
449 },
450 });
451 },
452 });
453
454 let diskaction_btn = new Proxmox.button.Button({
455 text: gettext('Disk Action'),
456 disabled: true,
457 menu: {
458 items: [
459 move_menuitem,
460 reassign_menuitem,
461 resize_menuitem,
462 ],
463 },
464 });
465
466
b49a0404 467 let remove_btn = new Proxmox.button.Button({
f97670f5 468 text: gettext('Remove'),
877fce54
EK
469 defaultText: gettext('Remove'),
470 altText: gettext('Detach'),
f97670f5
DM
471 selModel: sm,
472 disabled: true,
473 dangerous: true,
3b2e557f 474 RESTMethod: 'PUT',
f97670f5 475 confirmMsg: function(rec) {
cc80f765 476 let warn = gettext('Are you sure you want to remove entry {0}');
ff5a8a9b
EK
477 if (this.text === this.altText) {
478 warn = gettext('Are you sure you want to detach entry {0}');
479 }
cc80f765
TL
480 let rendered = me.renderKey(rec.data.key, {}, rec);
481 let msg = Ext.String.format(warn, `'${rendered}'`);
ff5a8a9b 482
cc80f765
TL
483 if (rows[rec.data.key].del_extra_msg) {
484 msg += '<br>' + rows[rec.data.key].del_extra_msg;
f97670f5 485 }
f97670f5
DM
486 return msg;
487 },
b49a0404 488 handler: function(btn, e, rec) {
e7ade592 489 Proxmox.Utils.API2Request({
f97670f5
DM
490 url: '/api2/extjs/' + baseurl,
491 waitMsgTarget: me,
b49a0404 492 method: btn.RESTMethod,
f97670f5 493 params: {
f6710aac 494 'delete': rec.data.key,
f97670f5 495 },
18f57a23 496 callback: () => me.reload(),
b49a0404 497 failure: response => Ext.Msg.alert('Error', response.htmlStatus),
3b2e557f 498 success: function(response, options) {
b49a0404
TL
499 if (btn.RESTMethod === 'POST') {
500 Ext.create('Proxmox.window.TaskProgress', {
501 autoShow: true,
502 upid: response.result.data,
3b2e557f 503 listeners: {
18f57a23 504 destroy: () => me.reload(),
f6710aac 505 },
3b2e557f 506 });
3b2e557f 507 }
f6710aac 508 },
f97670f5 509 });
877fce54
EK
510 },
511 listeners: {
512 render: function(btn) {
897c1fc8
TL
513 // hack: calculate the max button width on first display to prevent the whole
514 // toolbar to move when we switch between the "Remove" and "Detach" labels
877fce54
EK
515 var def = btn.getSize().width;
516
517 btn.setText(btn.altText);
518 var alt = btn.getSize().width;
519
520 btn.setText(btn.defaultText);
521
522 var optimal = alt > def ? alt : def;
523 btn.setSize({ width: optimal });
f6710aac
TL
524 },
525 },
f97670f5
DM
526 });
527
b49a0404 528 let revert_btn = new PVE.button.PendingRevert({
3ee15859 529 apiurl: '/api2/extjs/' + baseurl,
f97670f5
DM
530 });
531
b49a0404 532 let efidisk_menuitem = Ext.create('Ext.menu.Item', {
3b37ab6d 533 text: gettext('EFI Disk'),
a032a91a 534 iconCls: 'fa fa-fw fa-hdd-o black',
3b37ab6d
DC
535 disabled: !caps.vms['VM.Config.Disk'],
536 handler: function() {
b49a0404 537 let { data: bios } = me.rstore.getData().map.bios || {};
1535df62 538
b49a0404
TL
539 Ext.create('PVE.qemu.EFIDiskEdit', {
540 autoShow: true,
c570b6a9
TL
541 url: '/api2/extjs/' + baseurl,
542 pveSelNode: me.pveSelNode,
b49a0404
TL
543 usesEFI: bios?.value === 'ovmf' || bios?.pending === 'ovmf',
544 listeners: {
545 destroy: () => me.reload(),
546 },
c570b6a9 547 });
f6710aac 548 },
3b37ab6d
DC
549 });
550
67847d54 551 let counts = {};
53e3ea84 552 let isAtLimit = (type) => counts[type] >= PVE.Utils.hardware_counts[type];
17dcba38
DC
553 let isAtUsbLimit = () => {
554 let ostype = me.getObjectValue('ostype');
555 let machine = me.getObjectValue('machine');
556 return counts.usb >= PVE.Utils.get_max_usb_count(ostype, machine);
557 };
67847d54 558
b49a0404
TL
559 let set_button_status = function() {
560 let selection_model = me.getSelectionModel();
561 let rec = selection_model.getSelection()[0];
562
563 counts = {}; // en/disable hardwarebuttons
564 let hasCloudInit = false;
565 me.rstore.getData().items.forEach(function({ id, data }) {
566 if (!hasCloudInit && (isCloudInitKey(data.value) || isCloudInitKey(data.pending))) {
b38d0810 567 hasCloudInit = true;
67847d54
DC
568 return;
569 }
570
b49a0404 571 let match = id.match(/^([^\d]+)\d+$/);
67847d54 572 if (match && PVE.Utils.hardware_counts[match[1]] !== undefined) {
b49a0404
TL
573 let type = match[1];
574 counts[type] = (counts[type] || 0) + 1;
b38d0810 575 }
2668ba64 576 });
3097170d
TL
577
578 // heuristic only for disabling some stuff, the backend has the final word.
66abfe6a 579 const noSysConsolePerm = !caps.nodes['Sys.Console'];
7becf34f 580 const noHWPerm = !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'];
66abfe6a
AL
581 const noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType'];
582 const noVMConfigNetPerm = !caps.vms['VM.Config.Network'];
90b8c622 583 const noVMConfigDiskPerm = !caps.vms['VM.Config.Disk'];
ad1d539e
LN
584 const noVMConfigCDROMPerm = !caps.vms['VM.Config.CDROM'];
585 const noVMConfigCloudinitPerm = !caps.vms['VM.Config.Cloudinit'];
67847d54 586
7becf34f
DC
587 me.down('#addUsb').setDisabled(noHWPerm || isAtUsbLimit());
588 me.down('#addPci').setDisabled(noHWPerm || isAtLimit('hostpci'));
0bb1e60d
TL
589 me.down('#addAudio').setDisabled(noVMConfigHWTypePerm || isAtLimit('audio'));
590 me.down('#addSerial').setDisabled(noVMConfigHWTypePerm || isAtLimit('serial'));
591 me.down('#addNet').setDisabled(noVMConfigNetPerm || isAtLimit('net'));
592 me.down('#addRng').setDisabled(noSysConsolePerm || isAtLimit('rng'));
90b8c622 593 efidisk_menuitem.setDisabled(noVMConfigDiskPerm || isAtLimit('efidisk'));
0bb1e60d 594 me.down('#addTpmState').setDisabled(noSysConsolePerm || isAtLimit('tpmstate'));
ad1d539e 595 me.down('#addCloudinitDrive').setDisabled(noVMConfigCDROMPerm || noVMConfigCloudinitPerm || hasCloudInit);
3b37ab6d 596
f97670f5
DM
597 if (!rec) {
598 remove_btn.disable();
599 edit_btn.disable();
a8d854af 600 diskaction_btn.disable();
f97670f5
DM
601 revert_btn.disable();
602 return;
603 }
b49a0404 604 const { key, value } = rec.data;
96da47ba 605 const row = rows[key];
f97670f5 606
96da47ba
TL
607 const deleted = !!rec.data.delete;
608 const pending = deleted || me.hasPendingChanges(key);
f97670f5 609
b49a0404 610 const isCloudInit = isCloudInitKey(value);
ad1d539e 611 const isCDRom = value && !!value.toString().match(/media=cdrom/);
b38d0810 612
96da47ba 613 const isUnusedDisk = key.match(/^unused\d+/);
ad1d539e
LN
614 const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom;
615 const isDisk = isUnusedDisk || isUsedDisk;
96da47ba 616 const isEfi = key === 'efidisk0';
6d084964 617 const tpmMoveable = key === 'tpmstate0' && !me.pveSelNode.data.running;
3b37ab6d 618
5c63f8a1
TL
619 let cannotDelete = deleted || row.never_delete;
620 cannotDelete ||= isCDRom && !cdromCap;
621 cannotDelete ||= isDisk && !diskCap;
622 cannotDelete ||= isCloudInit && noVMConfigCloudinitPerm;
623 remove_btn.setDisabled(cannotDelete);
624
53e3ea84 625 remove_btn.setText(isUsedDisk && !isCloudInit ? remove_btn.altText : remove_btn.defaultText);
3b2e557f 626 remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT';
f97670f5 627
a1e25c47 628 edit_btn.setDisabled(
b49a0404 629 deleted || !row.editor || isCloudInit || (isCDRom && !cdromCap) || (isDisk && !diskCap));
f97670f5 630
a8d854af
AL
631 diskaction_btn.setDisabled(
632 pending ||
633 !diskCap ||
634 isCloudInit ||
635 !(isDisk || isEfi || tpmMoveable),
636 );
a8d854af
AL
637 reassign_menuitem.setDisabled(pending || (isEfi || tpmMoveable));
638 resize_menuitem.setDisabled(pending || !isUsedDisk);
f97670f5 639
f97670f5 640 revert_btn.setDisabled(!pending);
f97670f5
DM
641 };
642
aa8e5e79
TL
643 let editorFactory = (classPath, extraOptions) => {
644 extraOptions = extraOptions || {};
645 return () => Ext.create(`PVE.qemu.${classPath}`, {
646 autoShow: true,
647 url: `/api2/extjs/${baseurl}`,
648 pveSelNode: me.pveSelNode,
649 listeners: {
650 destroy: () => me.reload(),
651 },
652 isAdd: true,
653 isCreate: true,
654 ...extraOptions,
655 });
656 };
657
f09dd6c1 658 Ext.apply(me, {
87f53aa8 659 url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`,
f97670f5
DM
660 interval: 5000,
661 selModel: sm,
b08ae044 662 run_editor: run_editor,
2a4971d8 663 tbar: [
f97670f5
DM
664 {
665 text: gettext('Add'),
666 menu: new Ext.menu.Menu({
e08c2321 667 cls: 'pve-add-hw-menu',
f97670f5
DM
668 items: [
669 {
670 text: gettext('Hard Disk'),
a032a91a 671 iconCls: 'fa fa-fw fa-hdd-o black',
f97670f5 672 disabled: !caps.vms['VM.Config.Disk'],
aa8e5e79 673 handler: editorFactory('HDEdit'),
f97670f5
DM
674 },
675 {
676 text: gettext('CD/DVD Drive'),
677 iconCls: 'pve-itype-icon-cdrom',
a1e25c47 678 disabled: !caps.vms['VM.Config.CDROM'],
aa8e5e79 679 handler: editorFactory('CDEdit'),
f97670f5
DM
680 },
681 {
682 text: gettext('Network Device'),
0bb1e60d 683 itemId: 'addNet',
a032a91a 684 iconCls: 'fa fa-fw fa-exchange black',
f97670f5 685 disabled: !caps.vms['VM.Config.Network'],
aa8e5e79 686 handler: editorFactory('NetworkEdit'),
3b37ab6d 687 },
2668ba64 688 efidisk_menuitem,
6d084964
SR
689 {
690 text: gettext('TPM State'),
0bb1e60d 691 itemId: 'addTpmState',
6d084964
SR
692 iconCls: 'fa fa-fw fa-hdd-o black',
693 disabled: !caps.vms['VM.Config.Disk'],
aa8e5e79 694 handler: editorFactory('TPMDiskEdit'),
6d084964 695 },
2668ba64
DC
696 {
697 text: gettext('USB Device'),
0bb1e60d 698 itemId: 'addUsb',
a032a91a 699 iconCls: 'fa fa-fw fa-usb black',
7becf34f 700 disabled: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
aa8e5e79 701 handler: editorFactory('USBEdit'),
b38d0810 702 },
25a1063a
DC
703 {
704 text: gettext('PCI Device'),
0bb1e60d 705 itemId: 'addPci',
25a1063a 706 iconCls: 'pve-itype-icon-pci',
7becf34f 707 disabled: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
aa8e5e79 708 handler: editorFactory('PCIEdit'),
25a1063a 709 },
37977fa5
TL
710 {
711 text: gettext('Serial Port'),
0bb1e60d 712 itemId: 'addSerial',
37977fa5
TL
713 iconCls: 'pve-itype-icon-serial',
714 disabled: !caps.vms['VM.Config.Options'],
aa8e5e79 715 handler: editorFactory('SerialEdit'),
37977fa5 716 },
b38d0810
DC
717 {
718 text: gettext('CloudInit Drive'),
0bb1e60d 719 itemId: 'addCloudinitDrive',
a032a91a 720 iconCls: 'fa fa-fw fa-cloud black',
ad1d539e 721 disabled: !caps.vms['VM.Config.CDROM'] || !caps.vms['VM.Config.Cloudinit'],
aa8e5e79 722 handler: editorFactory('CIDriveEdit'),
171ca270
AL
723 },
724 {
725 text: gettext('Audio Device'),
0bb1e60d 726 itemId: 'addAudio',
a032a91a 727 iconCls: 'fa fa-fw fa-volume-up black',
171ca270 728 disabled: !caps.vms['VM.Config.HWType'],
aa8e5e79 729 handler: editorFactory('AudioEdit'),
6c1d8ead
SR
730 },
731 {
732 text: gettext("VirtIO RNG"),
0bb1e60d 733 itemId: 'addRng',
4ba4c7c6 734 iconCls: 'pve-itype-icon-die',
6c1d8ead 735 disabled: !caps.nodes['Sys.Console'],
aa8e5e79 736 handler: editorFactory('RNGEdit'),
f6710aac
TL
737 },
738 ],
739 }),
fa522e48 740 },
f97670f5
DM
741 remove_btn,
742 edit_btn,
a8d854af 743 diskaction_btn,
f6710aac 744 revert_btn,
f97670f5
DM
745 ],
746 rows: rows,
747 sorterFn: sorterFn,
748 listeners: {
749 itemdblclick: run_editor,
f6710aac
TL
750 selectionchange: set_button_status,
751 },
f97670f5
DM
752 });
753
754 me.callParent();
755
6386068d
TL
756 me.on('activate', me.rstore.startUpdate, me.rstore);
757 me.on('destroy', me.rstore.stopUpdate, me.rstore);
f97670f5 758
6386068d 759 me.mon(me.getStore(), 'datachanged', set_button_status, me);
f6710aac 760 },
f97670f5 761});