diff --git a/www/manager/lxc/ResourceEdit.js b/www/manager/lxc/ResourceEdit.js index 3bff0607..327c0657 100644 --- a/www/manager/lxc/ResourceEdit.js +++ b/www/manager/lxc/ResourceEdit.js @@ -35,6 +35,65 @@ Ext.define('PVE.lxc.CPUEdit', { } }); +Ext.define('PVE.lxc.MountPointEdit', { + extend: 'PVE.window.Edit', + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var unused = me.confid && me.confid.match(/^unused\d+$/); + + me.create = me.confid ? unused : true; + + var ipanel = Ext.create('PVE.lxc.MountPointInputPanel', { + confid: me.confid, + nodename: nodename, + unused: unused, + create: me.create, + }); + + var subject; + if (unused) { + subject = gettext('Unused Mount Point'); + } else if (me.create) { + subject = gettext('Mount Point'); + } else { + subject = gettext('Mount Point') + ' (' + me.confid + ')'; + } + + Ext.apply(me, { + subject: subject, + items: ipanel, + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + var value = response.result.data[me.confid]; + var mp = PVE.Parser.parseLxcMountPoint(value); + + if (!mp) { + Ext.Msg.alert(gettext('Error'), gettext('Unable to parse mount point options')); + me.close(); + return; + } + + ipanel.setMountPoint(mp); + me.isValid(); // trigger validation + } + } + }); + } +}); + Ext.define('PVE.lxc.CPUInputPanel', { extend: 'PVE.panel.InputPanel', alias: 'widget.pveLxcCPUInputPanel', @@ -120,3 +179,282 @@ Ext.define('PVE.lxc.MemoryInputPanel', { me.callParent(); } }); + +Ext.define('PVE.lxc.MountPointInputPanel', { + extend: 'PVE.panel.InputPanel', + alias: 'widget.pveLxcMountPointInputPanel', + + insideWizard: false, + + unused: false, // ADD usused disk imaged + + vmconfig: {}, // used to select usused disks + + onGetValues: function(values) { + var me = this; + + var confid = me.confid || values.mpsel; + + if (me.unused) { + me.mpdata.file = me.vmconfig[values.unusedId]; + confid = values.mpsel; + } else if (me.create) { + me.mpdata.file = values.storage + ':' + values.disksize; + } + + if (confid !== 'rootfs') + me.mpdata.mp = values.mp; + + if (values.ro) + me.mpdata.ro = 1; + else + delete me.mpdata.ro; + + if (values.quota) + me.mpdata.quota = 1; + else + delete me.mpdata.quota; + + if (values.acl === 'Default') + delete me.mpdata.acl; + else + me.mpdata.acl = values.acl; + + var res = {}; + res[confid] = PVE.Parser.printLxcMountPoint(me.mpdata); + return res; + }, + + setMountPoint: function(mp) { + var me = this; + + me.mpdata = mp; + if (!Ext.isDefined(me.mpdata['acl'])) { + me.mpdata['acl'] = 'Default'; + } + + if (mp.type === 'bind') { + me.quota.setDisabled(true); + me.quota.setValue(false); + } + + me.setValues(mp); + }, + + setVMConfig: function(vmconfig) { + var me = this; + + me.vmconfig = vmconfig; + + if (me.mpsel) { + for (var i = 0; i != 8; ++i) { + var name = "mp" + i; + if (!Ext.isDefined(vmconfig[name])) { + me.mpsel.setValue(name); + break; + } + } + } + + if (me.unusedDisks) { + var disklist = []; + Ext.Object.each(vmconfig, function(key, value) { + if (key.match(/^unused\d+$/)) { + disklist.push([key, value]); + } + }); + me.unusedDisks.store.loadData(disklist); + me.unusedDisks.setValue(me.confid); + } + }, + + setNodename: function(nodename) { + var me = this; + me.hdstoragesel.setNodename(nodename); + me.hdfilesel.setStorage(undefined, nodename); + }, + + initComponent : function() { + var me = this; + + var isroot = me.confid === 'rootfs'; + + me.mpdata = {}; + + me.column1 = []; + + if (!me.confid || me.unused) { + var names = []; + for (var i = 0; i != 8; ++i) { + var name = 'mp' + i; + names.push([name, name]); + } + me.mpsel = Ext.create('PVE.form.KVComboBox', { + name: 'mpsel', + fieldLabel: gettext('Mount Point'), + matchFieldWidth: false, + allowBlank: false, + data: names, + validator: function(value) { + if (!me.rendered) + return; + if (Ext.isDefined(me.vmconfig[value])) + return "Mount point is already in use."; + return true; + }, + listeners: { + change: function(field, value) { + field.validate(); + } + }, + }); + me.column1.push(me.mpsel); + } + + // we always have this around, but only visible when creating a new mp + // since this handles per-filesystem capabilities + me.hdstoragesel = Ext.create('PVE.form.StorageSelector', { + name: 'storage', + nodename: me.nodename, + fieldLabel: gettext('Storage'), + storageContent: 'rootdir', + allowBlank: false, + autoSelect: true, + hidden: me.unused || !me.create, + listeners: { + change: function(f, value) { + if (me.mpdata.type === 'bind') { + me.quota.setDisabled(true); + me.quota.setValue(false); + return; + } + var rec = f.store.getById(value); + if (rec.data.type === 'zfs' || + rec.data.type === 'zfspool') { + me.quota.setDisabled(true); + me.quota.setValue(false); + } else { + me.quota.setDisabled(false); + } + if (me.unused || !me.create) + return; + if (rec.data.type === 'iscsi') { + me.hdfilesel.setStorage(value); + me.hdfilesel.setDisabled(false); + me.hdfilesel.setVisible(true); + me.hdsizesel.setDisabled(true); + me.hdsizesel.setVisible(false); + } else if (rec.data.type === 'lvm' || + rec.data.type === 'lvmthin' || + rec.data.type === 'rbd' || + rec.data.type === 'sheepdog' || + rec.data.type === 'zfs' || + rec.data.type === 'zfspool') { + me.hdfilesel.setDisabled(true); + me.hdfilesel.setVisible(false); + me.hdsizesel.setDisabled(false); + me.hdsizesel.setVisible(true); + } else { + me.hdfilesel.setDisabled(true); + me.hdfilesel.setVisible(false); + me.hdsizesel.setDisabled(false); + me.hdsizesel.setVisible(true); + } + }, + }, + }); + me.column1.push(me.hdstoragesel); + + if (me.unused) { + me.unusedDisks = Ext.create('PVE.form.KVComboBox', { + name: 'unusedId', + fieldLabel: gettext('Disk image'), + matchFieldWidth: false, + listConfig: { + width: 350 + }, + data: [], + allowBlank: false, + listeners: { + change: function(f, value) { + // make sure our buttons are enabled/disabled when switching + // between images on different storages: + var disk = me.vmconfig[value]; + var storage = disk.split(':')[0]; + me.hdstoragesel.setValue(storage); + }, + }, + }); + me.column1.push(me.unusedDisks); + } else if (me.create) { + me.hdfilesel = Ext.create('PVE.form.FileSelector', { + name: 'file', + nodename: me.nodename, + storageContent: 'images', + fieldLabel: gettext('Disk image'), + disabled: true, + hidden: true, + allowBlank: false + }); + me.hdsizesel = Ext.createWidget('numberfield', { + name: 'disksize', + minValue: 0.1, + maxValue: 128*1024, + decimalPrecision: 3, + value: '8', + step: 1, + fieldLabel: gettext('Disk size') + ' (GB)', + allowBlank: false + }); + me.column1.push(me.hdfilesel); + me.column1.push(me.hdsizesel); + } else { + me.column1.push({ + xtype: 'textfield', + disabled: true, + submitValue: false, + fieldLabel: gettext('Disk image'), + name: 'file' + }); + } + + me.quota = Ext.createWidget('pvecheckbox', { + name: 'quota', + defaultValue: 0, + fieldLabel: gettext('Enable quota'), + }); + + me.column2 = [ + { + xtype: 'pvecheckbox', + name: 'ro', + defaultValue: 0, + fieldLabel: gettext('Read-only'), + hidden: me.insideWizard, + }, + { + xtype: 'pveKVComboBox', + name: 'acl', + fieldLabel: gettext('ACLs'), + data: [['Default', 'Default'], ['1', 'On'], ['0', 'Off']], + value: 'Default', + allowBlank: true, + }, + me.quota, + ]; + + if (!isroot) { + me.column2.push({ + xtype: 'textfield', + name: 'mp', + value: '', + emptyText: gettext('Path'), + allowBlank: false, + hidden: isroot, + fieldLabel: gettext('/some/path'), + }); + }; + + me.callParent(); + } +}); diff --git a/www/manager/lxc/Resources.js b/www/manager/lxc/Resources.js index 7149ff0e..396b6f9a 100644 --- a/www/manager/lxc/Resources.js +++ b/www/manager/lxc/Resources.js @@ -35,6 +35,8 @@ Ext.define('PVE.lxc.RessourceView', { var caps = Ext.state.Manager.get('GuiCap'); + var mpeditor = caps.vms['VM.Config.Disk'] ? 'PVE.lxc.MountPointEdit' : undefined; + var rows = { memory: { header: gettext('Memory'), @@ -77,6 +79,7 @@ Ext.define('PVE.lxc.RessourceView', { rootfs: { header: gettext('Root Disk'), defaultValue: PVE.Utils.noneText, + editor: mpeditor, tdCls: 'pve-itype-icon-storage' } }; @@ -86,10 +89,21 @@ Ext.define('PVE.lxc.RessourceView', { rows[confid] = { group: 1, tdCls: 'pve-itype-icon-storage', + editor: mpeditor, header: gettext('Mount Point') + ' (' + confid +')', }; } + for (i = 0; i < 8; i++) { + confid = "unused" + i; + rows[confid] = { + group: 1, + tdCls: 'pve-itype-icon-storage', + editor: mpeditor, + header: gettext('Unused Disk') + ' ' + i, + }; + } + var reload = function() { me.rstore.load(); }; @@ -138,6 +152,23 @@ Ext.define('PVE.lxc.RessourceView', { win.on('destroy', reload); }; + var run_remove = function(b, e, rec) { + PVE.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'delete': rec.data.key + }, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + var edit_btn = new PVE.button.Button({ text: gettext('Edit'), selModel: sm, @@ -159,12 +190,30 @@ Ext.define('PVE.lxc.RessourceView', { handler: run_resize }); + var remove_btn = new PVE.button.Button({ + text: gettext('Remove'), + selModel: sm, + disabled: true, + dangerous: true, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + me.renderKey(rec.data.key, {}, rec) + "'"); + if (rec.data.key.match(/^unused\d+$/)) { + msg += " " + gettext('This will permanently erase all image data.'); + } + + return msg; + }, + handler: run_remove + }); + var set_button_status = function() { var sm = me.getSelectionModel(); var rec = sm.getSelection()[0]; if (!rec) { edit_btn.disable(); + remove_btn.disable(); resize_btn.disable(); return; } @@ -176,6 +225,7 @@ Ext.define('PVE.lxc.RessourceView', { edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor); + remove_btn.setDisabled(!isDisk || rec.data.key === 'rootfs'); resize_btn.setDisabled(!isDisk); }; @@ -184,8 +234,31 @@ Ext.define('PVE.lxc.RessourceView', { url: '/api2/json/' + baseurl, selModel: sm, cwidth1: 170, - tbar: [ edit_btn, - resize_btn], + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Mount Point'), + iconCls: 'pve-itype-icon-storage', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + var win = Ext.create('PVE.lxc.MountPointEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } + }, + ] + }) + }, + edit_btn, + remove_btn, + resize_btn, + ], rows: rows, listeners: { show: reload,