]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/lxc/Resources.js
ui: lxc: add edit window for device passthrough
[pve-manager.git] / www / manager6 / lxc / Resources.js
CommitLineData
c7ee0c11 1Ext.define('PVE.lxc.RessourceView', {
9fd8d73e 2 extend: 'Proxmox.grid.PendingObjectGrid',
c7ee0c11
DM
3 alias: ['widget.pveLxcRessourceView'],
4
ba93a9c6
DC
5 onlineHelp: 'pct_configuration',
6
c7ee0c11 7 renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
02ecbc98
TL
8 let me = this;
9 let rowdef = me.rows[key] || {};
10
11 let txt = rowdef.header || key;
12 let icon = '';
c7ee0c11
DM
13
14 metaData.tdAttr = "valign=middle";
c7ee0c11
DM
15 if (rowdef.tdCls) {
16 metaData.tdCls = rowdef.tdCls;
02ecbc98
TL
17 } else if (rowdef.iconCls) {
18 icon = `<i class='pve-grid-fa fa fa-fw fa-${rowdef.iconCls}'></i>`;
19 metaData.tdCls += " pve-itype-fa";
20 }
21 // only return icons in grid but not remove dialog
22 if (rowIndex !== undefined) {
23 return icon + txt;
24 } else {
25 return txt;
c7ee0c11 26 }
c7ee0c11
DM
27 },
28
8058410f 29 initComponent: function() {
c7ee0c11 30 var me = this;
55ee6ba1 31 let confid;
c7ee0c11
DM
32
33 var nodename = me.pveSelNode.data.node;
2a4971d8 34 if (!nodename) {
c7ee0c11
DM
35 throw "no node name specified";
36 }
37
38 var vmid = me.pveSelNode.data.vmid;
39 if (!vmid) {
40 throw "no VM ID specified";
41 }
42
43 var caps = Ext.state.Manager.get('GuiCap');
d35b5b2a 44 var diskCap = caps.vms['VM.Config.Disk'];
c7ee0c11
DM
45
46 var mpeditor = caps.vms['VM.Config.Disk'] ? 'PVE.lxc.MountPointEdit' : undefined;
47
5a2e333c
FE
48 const nodeInfo = PVE.data.ResourceStore.getNodes().find(node => node.node === nodename);
49 let cpuEditor = {
50 xtype: 'pveLxcCPUEdit',
51 cgroupMode: nodeInfo['cgroup-mode'],
52 };
53
c7ee0c11
DM
54 var rows = {
55 memory: {
56 header: gettext('Memory'),
57 editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
c7ee0c11 58 defaultValue: 512,
a8ea1b68 59 tdCls: 'pmx-itype-icon-memory',
1a2fdf62 60 group: 1,
c7ee0c11 61 renderer: function(value) {
e7ade592 62 return Proxmox.Utils.format_size(value*1024*1024);
f6710aac 63 },
c7ee0c11
DM
64 },
65 swap: {
66 header: gettext('Swap'),
67 editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
c7ee0c11 68 defaultValue: 512,
809f6b6e 69 iconCls: 'refresh',
1a2fdf62 70 group: 2,
c7ee0c11 71 renderer: function(value) {
e7ade592 72 return Proxmox.Utils.format_size(value*1024*1024);
f6710aac 73 },
c7ee0c11 74 },
92b5029f
DM
75 cores: {
76 header: gettext('Cores'),
5a2e333c 77 editor: caps.vms['VM.Config.CPU'] ? cpuEditor : undefined,
92b5029f 78 defaultValue: '',
a8ea1b68 79 tdCls: 'pmx-itype-icon-processor',
1a2fdf62 80 group: 3,
92b5029f 81 renderer: function(value) {
3e4b78dc
DC
82 var cpulimit = me.getObjectValue('cpulimit');
83 var cpuunits = me.getObjectValue('cpuunits');
84 var res;
85 if (value) {
86 res = value;
87 } else {
88 res = gettext('unlimited');
89 }
90
91 if (cpulimit) {
92 res += ' [cpulimit=' + cpulimit + ']';
93 }
94
95 if (cpuunits) {
96 res += ' [cpuunits=' + cpuunits + ']';
97 }
98 return res;
f6710aac 99 },
92b5029f 100 },
c7ee0c11
DM
101 rootfs: {
102 header: gettext('Root Disk'),
e7ade592 103 defaultValue: Proxmox.Utils.noneText,
c7ee0c11 104 editor: mpeditor,
809f6b6e 105 iconCls: 'hdd-o',
f6710aac 106 group: 4,
c15e9cd5 107 },
3e4b78dc 108 cpulimit: {
f6710aac 109 visible: false,
3e4b78dc
DC
110 },
111 cpuunits: {
f6710aac 112 visible: false,
3e4b78dc 113 },
c15e9cd5 114 unprivileged: {
f6710aac
TL
115 visible: false,
116 },
c7ee0c11
DM
117 };
118
3f40e121 119 PVE.Utils.forEachLxcMP(function(bus, i) {
14a845bc 120 confid = bus + i;
1a2fdf62
DL
121 var group = 5;
122 var header;
14a845bc
DC
123 if (bus === 'mp') {
124 header = gettext('Mount Point') + ' (' + confid + ')';
125 } else {
126 header = gettext('Unused Disk') + ' ' + i;
1a2fdf62 127 group += 1;
14a845bc 128 }
c7ee0c11 129 rows[confid] = {
1a2fdf62
DL
130 group: group,
131 order: i,
c7ee0c11
DM
132 tdCls: 'pve-itype-icon-storage',
133 editor: mpeditor,
f6710aac 134 header: header,
c7ee0c11 135 };
14a845bc 136 }, true);
c7ee0c11 137
4c406fed
FS
138 let deveditor = Proxmox.UserName === 'root@pam' ? 'PVE.lxc.DeviceEdit' : undefined;
139
140 PVE.Utils.forEachLxcDev(function(i) {
141 confid = 'dev' + i;
142 rows[confid] = {
143 group: 7,
144 order: i,
145 tdCls: 'pve-itype-icon-pci',
146 editor: deveditor,
147 header: gettext('Device') + ' (' + confid + ')',
148 };
149 });
150
c7ee0c11
DM
151 var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
152
14dd743b 153 me.selModel = Ext.create('Ext.selection.RowModel', {});
c7ee0c11
DM
154
155 var run_resize = function() {
14dd743b 156 var rec = me.selModel.getSelection()[0];
c7ee0c11
DM
157 if (!rec) {
158 return;
159 }
160
161 var win = Ext.create('PVE.window.MPResize', {
162 disk: rec.data.key,
163 nodename: nodename,
f6710aac 164 vmid: vmid,
c7ee0c11
DM
165 });
166
167 win.show();
c7ee0c11
DM
168 };
169
170 var run_remove = function(b, e, rec) {
e7ade592 171 Proxmox.Utils.API2Request({
c7ee0c11
DM
172 url: '/api2/extjs/' + baseurl,
173 waitMsgTarget: me,
174 method: 'PUT',
175 params: {
f6710aac 176 'delete': rec.data.key,
c7ee0c11 177 },
8058410f 178 failure: function(response, opts) {
c7ee0c11 179 Ext.Msg.alert('Error', response.htmlStatus);
f6710aac 180 },
c7ee0c11
DM
181 });
182 };
183
a8d854af
AL
184 let run_move = function() {
185 let rec = me.selModel.getSelection()[0];
c7164db7
DC
186 if (!rec) {
187 return;
188 }
189
190 var win = Ext.create('PVE.window.HDMove', {
191 disk: rec.data.key,
192 nodename: nodename,
193 vmid: vmid,
f6710aac 194 type: 'lxc',
c7164db7
DC
195 });
196
197 win.show();
198
199 win.on('destroy', me.reload, me);
200 };
201
a8d854af
AL
202 let run_reassign = function() {
203 let rec = me.selModel.getSelection()[0];
204 if (!rec) {
205 return;
206 }
207
3bde324f 208 Ext.create('PVE.window.GuestDiskReassign', {
a8d854af
AL
209 disk: rec.data.key,
210 nodename: nodename,
211 autoShow: true,
212 vmid: vmid,
213 type: 'lxc',
214 listeners: {
215 destroy: () => me.reload(),
216 },
217 });
218 };
219
5720fafa 220 var edit_btn = new Proxmox.button.Button({
c7ee0c11 221 text: gettext('Edit'),
14dd743b 222 selModel: me.selModel,
c7ee0c11
DM
223 disabled: true,
224 enableFn: function(rec) {
225 if (!rec) {
226 return false;
227 }
228 var rowdef = rows[rec.data.key];
229 return !!rowdef.editor;
230 },
f6710aac 231 handler: function() { me.run_editor(); },
c7ee0c11
DM
232 });
233
5720fafa 234 var remove_btn = new Proxmox.button.Button({
c7ee0c11 235 text: gettext('Remove'),
986ccfe4
AL
236 defaultText: gettext('Remove'),
237 altText: gettext('Detach'),
14dd743b 238 selModel: me.selModel,
c7ee0c11
DM
239 disabled: true,
240 dangerous: true,
241 confirmMsg: function(rec) {
986ccfe4
AL
242 let warn = Ext.String.format(gettext('Are you sure you want to remove entry {0}'));
243 if (this.text === this.altText) {
244 warn = gettext('Are you sure you want to detach entry {0}');
245 }
cc80f765
TL
246 let rendered = me.renderKey(rec.data.key, {}, rec);
247 let msg = Ext.String.format(warn, `'${rendered}'`);
986ccfe4 248
c7ee0c11 249 if (rec.data.key.match(/^unused\d+$/)) {
16152937 250 msg += " " + gettext('This will permanently erase all data.');
c7ee0c11 251 }
c7ee0c11
DM
252 return msg;
253 },
f6710aac 254 handler: run_remove,
986ccfe4
AL
255 listeners: {
256 render: function(btn) {
257 // hack: calculate the max button width on first display to prevent the whole
258 // toolbar to move when we switch between the "Remove" and "Detach" labels
259 let def = btn.getSize().width;
260
261 btn.setText(btn.altText);
262 let alt = btn.getSize().width;
263
264 btn.setText(btn.defaultText);
265
266 let optimal = alt > def ? alt : def;
267 btn.setSize({ width: optimal });
268 },
269 },
c7ee0c11
DM
270 });
271
a8d854af
AL
272 let move_menuitem = new Ext.menu.Item({
273 text: gettext('Move Storage'),
274 tooltip: gettext('Move volume to another storage'),
275 iconCls: 'fa fa-database',
c7164db7 276 selModel: me.selModel,
f6710aac 277 handler: run_move,
c7164db7
DC
278 });
279
a8d854af
AL
280 let reassign_menuitem = new Ext.menu.Item({
281 text: gettext('Reassign Owner'),
282 tooltip: gettext('Reassign volume to another CT'),
283 iconCls: 'fa fa-cube',
284 handler: run_reassign,
285 reference: 'reassing_item',
286 });
287
288 let resize_menuitem = new Ext.menu.Item({
289 text: gettext('Resize'),
290 iconCls: 'fa fa-plus',
291 selModel: me.selModel,
292 handler: run_resize,
293 });
294
295 let volumeaction_btn = new Proxmox.button.Button({
296 text: gettext('Volume Action'),
297 disabled: true,
298 menu: {
299 items: [
300 move_menuitem,
301 reassign_menuitem,
302 resize_menuitem,
303 ],
304 },
305 });
306
716c3043 307 let revert_btn = new PVE.button.PendingRevert();
273b5ce3 308
716c3043
TL
309 let set_button_status = function() {
310 let rec = me.selModel.getSelection()[0];
c7ee0c11
DM
311
312 if (!rec) {
313 edit_btn.disable();
314 remove_btn.disable();
a8d854af 315 volumeaction_btn.disable();
273b5ce3 316 revert_btn.disable();
c7ee0c11
DM
317 return;
318 }
716c3043
TL
319 let { key, value, 'delete': isDelete } = rec.data;
320 let rowdef = rows[key];
c7ee0c11 321
716c3043 322 let pending = isDelete || me.hasPendingChanges(key);
a8d854af
AL
323 let isRootFS = key === 'rootfs';
324 let isDisk = isRootFS || key.match(/^(mp|unused)\d+/);
716c3043
TL
325 let isUnusedDisk = key.match(/^unused\d+/);
326 let isUsedDisk = isDisk && !isUnusedDisk;
4c406fed 327 let isDevice = key.match(/^dev\d+/);
c7ee0c11 328
716c3043 329 let noedit = isDelete || !rowdef.editor;
35a04562 330 if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) {
716c3043 331 let mp = PVE.Parser.parseLxcMountPoint(value);
c7ee0c11
DM
332 if (mp.type !== 'volume') {
333 noedit = true;
334 }
335 }
336 edit_btn.setDisabled(noedit);
337
a8d854af
AL
338 volumeaction_btn.setDisabled(!isDisk || !diskCap);
339 move_menuitem.setDisabled(isUnusedDisk);
340 reassign_menuitem.setDisabled(isRootFS);
341 resize_menuitem.setDisabled(isUnusedDisk);
342
4c406fed 343 remove_btn.setDisabled(!(isDisk || isDevice) || isRootFS || !diskCap || pending);
273b5ce3 344 revert_btn.setDisabled(!pending);
986ccfe4
AL
345
346 remove_btn.setText(isUsedDisk ? remove_btn.altText : remove_btn.defaultText);
c7ee0c11 347 };
2a4971d8 348
716c3043
TL
349 let sorterFn = function(rec1, rec2) {
350 let v1 = rec1.data.key, v2 = rec2.data.key;
1a2fdf62 351
716c3043 352 let g1 = rows[v1].group || 0, g2 = rows[v2].group || 0;
53e3ea84 353 if (g1 - g2 !== 0) {
1a2fdf62
DL
354 return g1 - g2;
355 }
356
716c3043 357 let order1 = rows[v1].order || 0, order2 = rows[v2].order || 0;
53e3ea84 358 if (order1 - order2 !== 0) {
1a2fdf62
DL
359 return order1 - order2;
360 }
361
362 if (v1 > v2) {
363 return 1;
364 } else if (v1 < v2) {
365 return -1;
366 } else {
367 return 0;
368 }
93bd0d75 369 };
1a2fdf62 370
f7993618 371 Ext.apply(me, {
716c3043 372 url: `/api2/json/nodes/${nodename}/lxc/${vmid}/pending`,
14dd743b
TL
373 selModel: me.selModel,
374 interval: 2000,
c7ee0c11
DM
375 cwidth1: 170,
376 tbar: [
377 {
378 text: gettext('Add'),
379 menu: new Ext.menu.Menu({
380 items: [
381 {
382 text: gettext('Mount Point'),
809f6b6e 383 iconCls: 'fa fa-fw fa-hdd-o black',
c7ee0c11
DM
384 disabled: !caps.vms['VM.Config.Disk'],
385 handler: function() {
163d9f17
TL
386 Ext.create('PVE.lxc.MountPointEdit', {
387 autoShow: true,
388 url: `/api2/extjs/${baseurl}`,
c15e9cd5 389 unprivileged: me.getObjectValue('unprivileged'),
f6710aac 390 pveSelNode: me.pveSelNode,
163d9f17
TL
391 listeners: {
392 destroy: () => me.reload(),
393 },
c7ee0c11 394 });
f6710aac
TL
395 },
396 },
4c406fed
FS
397 {
398 text: gettext('Device Passthrough'),
399 iconCls: 'pve-itype-icon-pci',
400 disabled: Proxmox.UserName !== 'root@pam',
401 handler: function() {
402 Ext.create('PVE.lxc.DeviceEdit', {
403 autoShow: true,
404 url: `/api2/extjs/${baseurl}`,
405 pveSelNode: me.pveSelNode,
406 listeners: {
407 destroy: () => me.reload(),
408 },
409 });
410 },
411 },
f6710aac
TL
412 ],
413 }),
c7ee0c11
DM
414 },
415 edit_btn,
416 remove_btn,
a8d854af 417 volumeaction_btn,
f6710aac 418 revert_btn,
c7ee0c11
DM
419 ],
420 rows: rows,
1a2fdf62 421 sorterFn: sorterFn,
14dd743b
TL
422 editorConfig: {
423 pveSelNode: me.pveSelNode,
f6710aac 424 url: '/api2/extjs/' + baseurl,
14dd743b 425 },
c7ee0c11 426 listeners: {
14dd743b 427 itemdblclick: me.run_editor,
f6710aac
TL
428 selectionchange: set_button_status,
429 },
c7ee0c11
DM
430 });
431
432 me.callParent();
14dd743b
TL
433
434 me.on('activate', me.rstore.startUpdate);
435 me.on('destroy', me.rstore.stopUpdate);
436 me.on('deactivate', me.rstore.stopUpdate);
437
69f36699
DC
438 me.mon(me.getStore(), 'datachanged', function() {
439 set_button_status();
440 });
441
14dd743b 442 Ext.apply(me.editorConfig, { unprivileged: me.getObjectValue('unprivileged') });
f6710aac 443 },
c7ee0c11 444});