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