diff --git a/www/manager5/grid/FirewallRules.js b/www/manager5/grid/FirewallRules.js new file mode 100644 index 00000000..ae536379 --- /dev/null +++ b/www/manager5/grid/FirewallRules.js @@ -0,0 +1,776 @@ +Ext.define('PVE.form.FWMacroSelector', { + extend: 'PVE.form.ComboGrid', + alias: 'widget.pveFWMacroSelector', + + initComponent: function() { + var me = this; + + var store = Ext.create('Ext.data.Store', { + autoLoad: true, + fields: [ 'macro', 'descr' ], + idProperty: 'macro', + proxy: { + type: 'pve', + url: "/api2/json/cluster/firewall/macros" + }, + sorters: { + property: 'macro', + order: 'DESC' + } + }); + + Ext.apply(me, { + store: store, + allowBlank: true, + autoSelect: false, + valueField: 'macro', + displayField: 'macro', + listConfig: { + columns: [ + { + header: gettext('Macro'), + dataIndex: 'macro', + hideable: false, + width: 100 + }, + { + header: gettext('Description'), + flex: 1, + dataIndex: 'descr' + } + ] + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.FirewallRulePanel', { + extend: 'PVE.panel.InputPanel', + + allow_iface: false, + + list_refs_url: undefined, + + onGetValues: function(values) { + var me = this; + + // hack: editable ComboGrid returns nothing when empty, so we need to set '' + // Also, disabled text fields return nothing, so we need to set '' + + Ext.Array.each(['source', 'dest', 'proto', 'sport', 'dport'], function(key) { + if (values[key] === undefined) { + values[key] = ''; + } + }); + + delete values.modified_marker; + + return values; + }, + + initComponent : function() { + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + me.column1 = [ + { + // hack: we use this field to mark the form 'dirty' when the + // record has errors- so that the user can safe the unmodified + // form again. + xtype: 'hiddenfield', + name: 'modified_marker', + value: '', + }, + { + xtype: 'pveKVComboBox', + name: 'type', + value: 'in', + data: [['in', 'in'], ['out', 'out']], + fieldLabel: gettext('Direction'), + allowBlank: false + }, + { + xtype: 'pveKVComboBox', + name: 'action', + value: 'ACCEPT', + data: [['ACCEPT', 'ACCEPT'], ['DROP', 'DROP'], ['REJECT', 'REJECT']], + fieldLabel: gettext('Action'), + allowBlank: false + } + ]; + + if (me.allow_iface) { + me.column1.push({ + xtype: 'pvetextfield', + name: 'iface', + deleteEmpty: !me.create, + value: '', + fieldLabel: gettext('Interface') + }); + } else { + me.column1.push({ + xtype: 'displayfield', + fieldLabel: '', + height: 22, // hack: set same height as text fields + value: '' + }); + } + + me.column1.push([ + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '' + }, + { + xtype: 'pveIPRefSelector', + name: 'source', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('Source') + + }, + { + xtype: 'pveIPRefSelector', + name: 'dest', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('Destination') + } + ]); + + + me.column2 = [ + { + xtype: 'pvecheckbox', + name: 'enable', + checked: false, + height: 22, // hack: set same height as text fields + uncheckedValue: 0, + fieldLabel: gettext('Enable') + }, + { + xtype: 'pveFWMacroSelector', + name: 'macro', + value: '', + fieldLabel: gettext('Macro'), + allowBlank: true, + listeners: { + change: function(f, value) { + if (value === '') { + me.down('field[name=proto]').setDisabled(false); + me.down('field[name=sport]').setDisabled(false); + me.down('field[name=dport]').setDisabled(false); + } else { + me.down('field[name=proto]').setDisabled(true); + me.down('field[name=proto]').setValue(''); + me.down('field[name=sport]').setDisabled(true); + me.down('field[name=sport]').setValue(''); + me.down('field[name=dport]').setDisabled(true); + me.down('field[name=dport]').setValue(''); + } + } + } + }, + { + xtype: 'pveIPProtocolSelector', + name: 'proto', + autoSelect: false, + editable: true, + value: '', + fieldLabel: gettext('Protocol') + }, + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '' + }, + { + xtype: 'textfield', + name: 'sport', + value: '', + fieldLabel: gettext('Source port') + }, + { + xtype: 'textfield', + name: 'dport', + height: 22, // hack: set same height as text fields + value: '', + fieldLabel: gettext('Dest. port') + } + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.FirewallRuleEdit', { + extend: 'PVE.window.Edit', + + base_url: undefined, + list_refs_url: undefined, + + allow_iface: false, + + initComponent : function() { + /*jslint confusion: true */ + var me = this; + + if (!me.base_url) { + throw "no base_url specified"; + } + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + me.create = (me.rule_pos === undefined); + + if (me.create) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString(); + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.FirewallRulePanel', { + create: me.create, + list_refs_url: me.list_refs_url, + allow_iface: me.allow_iface, + rule_pos: me.rule_pos + }); + + Ext.apply(me, { + subject: gettext('Rule'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.create) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + if (values.errors) { + var field = me.query('[isFormField][name=modified_marker]')[0]; + field.setValue(1); + Ext.Function.defer(function() { + var form = ipanel.up('form').getForm(); + form.markInvalid(values.errors) + }, 100); + } + } + }); + } + } +}); + +Ext.define('PVE.FirewallGroupRuleEdit', { + extend: 'PVE.window.Edit', + + base_url: undefined, + + allow_iface: false, + + initComponent : function() { + /*jslint confusion: true */ + var me = this; + + me.create = (me.rule_pos === undefined); + + if (me.create) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString(); + me.method = 'PUT'; + } + + var column1 = [ + { + xtype: 'hiddenfield', + name: 'type', + value: 'group' + }, + { + xtype: 'pveSecurityGroupsSelector', + name: 'action', + value: '', + fieldLabel: gettext('Security Group'), + allowBlank: false + } + ]; + + if (me.allow_iface) { + column1.push({ + xtype: 'pvetextfield', + name: 'iface', + deleteEmpty: !me.create, + value: '', + fieldLabel: gettext('Interface') + }); + } + + var ipanel = Ext.create('PVE.panel.InputPanel', { + create: me.create, + column1: column1, + column2: [ + { + xtype: 'pvecheckbox', + name: 'enable', + checked: false, + height: 22, // hack: set same height as text fields + uncheckedValue: 0, + fieldLabel: gettext('Enable') + } + ], + columnB: [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + + Ext.apply(me, { + subject: gettext('Rule'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.create) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + } + }); + } + } +}); + +Ext.define('PVE.FirewallRules', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveFirewallRules', + + base_url: undefined, + list_refs_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + groupBtn: undefined, + + tbar_prefix: undefined, + + allow_groups: true, + allow_iface: false, + + setBaseUrl: function(url) { + var me = this; + + me.base_url = url; + + if (url === undefined) { + me.addBtn.setDisabled(true); + if (me.groupBtn) { + me.groupBtn.setDisabled(true); + } + me.store.removeAll(); + } else { + me.addBtn.setDisabled(false); + if (me.groupBtn) { + me.groupBtn.setDisabled(false); + } + me.store.setProxy({ + type: 'pve', + url: '/api2/json' + url + }); + + me.store.load(); + } + }, + + moveRule: function(from, to) { + var me = this; + + if (!me.base_url) { + return; + } + + PVE.Utils.API2Request({ + url: me.base_url + "/" + from, + method: 'PUT', + params: { moveto: to }, + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: function() { + me.store.load(); + } + }); + }, + + updateRule: function(rule) { + var me = this; + + if (!me.base_url) { + return; + } + + rule.enable = rule.enable ? 1 : 0; + + var pos = rule.pos; + delete rule.pos; + delete rule.errors; + + PVE.Utils.API2Request({ + url: me.base_url + '/' + pos.toString(), + method: 'PUT', + params: rule, + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: function() { + me.store.load(); + } + }); + }, + + deleteRule: function(rule) { + var me = this; + + if (!me.base_url) { + return; + } + + PVE.Utils.API2Request({ + url: me.base_url + '/' + rule.pos.toString() + + '?digest=' + encodeURIComponent(rule.digest), + method: 'DELETE', + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: function() { + me.store.load(); + } + }); + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-fw-rule' + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var type = rec.data.type; + + var editor; + if (type === 'in' || type === 'out') { + editor = 'PVE.FirewallRuleEdit'; + } else if (type === 'group') { + editor = 'PVE.FirewallGroupRuleEdit'; + } else { + return; + } + + var win = Ext.create(editor, { + digest: rec.data.digest, + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url, + rule_pos: rec.data.pos + }); + + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new PVE.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = Ext.create('Ext.Button', { + text: gettext('Add'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.FirewallRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url + }); + win.on('destroy', reload); + win.show(); + } + }); + + if (me.allow_groups) { + me.groupBtn = Ext.create('Ext.Button', { + text: gettext('Insert') + ': ' + + gettext('Security Group'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.FirewallGroupRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url + }); + win.on('destroy', reload); + win.show(); + } + }); + } + + me.removeBtn = new PVE.button.Button({ + text: gettext('Remove'), + selModel: sm, + disabled: true, + handler: function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + me.deleteRule(rec.data); + } + }); + + var tbar = me.tbar_prefix ? [ me.tbar_prefix ] : []; + tbar.push(me.addBtn); + if (me.groupBtn) { + tbar.push(me.groupBtn); + } + tbar.push([ me.removeBtn, me.editBtn ]); + + var render_errors = function(name, value, metaData, record) { + var errors = record.data.errors; + if (errors && errors[name]) { + metaData.tdCls = 'x-form-invalid-field'; + var html = '

' + Ext.htmlEncode(errors[name]) + '

'; + metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + + html.replace(/\"/g,'"') + '"'; + } + return value; + }; + + var columns = [ + { + // similar to xtype: 'rownumberer', + dataIndex: 'pos', + resizable: false, + width: 23, + sortable: false, + align: 'right', + hideable: false, + menuDisabled: true, + renderer: function(value, metaData, record, rowIdx, colIdx, store) { + metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special'; + if (value >= 0) { + return value; + } + return ''; + } + }, + { + xtype: 'checkcolumn', + header: gettext('Enable'), + dataIndex: 'enable', + listeners: { + checkchange: function(column, record, checked) { + record.commit(); + var data = {}; + record.fields.each(function(field) { + data[field.name] = record.get(field.name); + }); + if (!me.allow_iface || !data.iface) { + delete data.iface; + } + me.updateRule(data); + } + }, + width: 50 + }, + { + header: gettext('Type'), + dataIndex: 'type', + renderer: function(value, metaData, record) { + return render_errors('type', value, metaData, record); + }, + width: 50 + }, + { + header: gettext('Action'), + dataIndex: 'action', + renderer: function(value, metaData, record) { + return render_errors('action', value, metaData, record); + }, + width: 80 + }, + { + header: gettext('Macro'), + dataIndex: 'macro', + renderer: function(value, metaData, record) { + return render_errors('macro', value, metaData, record); + }, + width: 80 + } + ]; + + if (me.allow_iface) { + columns.push({ + header: gettext('Interface'), + dataIndex: 'iface', + renderer: function(value, metaData, record) { + return render_errors('iface', value, metaData, record); + }, + width: 80 + }); + } + + columns.push([ + { + header: gettext('Source'), + dataIndex: 'source', + renderer: function(value, metaData, record) { + return render_errors('source', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Destination'), + dataIndex: 'dest', + renderer: function(value, metaData, record) { + return render_errors('dest', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Protocol'), + dataIndex: 'proto', + renderer: function(value, metaData, record) { + return render_errors('proto', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Dest. port'), + dataIndex: 'dport', + renderer: function(value, metaData, record) { + return render_errors('dport', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Source port'), + dataIndex: 'sport', + renderer: function(value, metaData, record) { + return render_errors('sport', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + flex: 1, + renderer: function(value, metaData, record) { + return render_errors('comment', Ext.util.Format.htmlEncode(value), metaData, record); + } + } + ]); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + plugins: [ + { + ptype: 'gridviewdragdrop', + dragGroup: 'FWRuleDDGroup', + dropGroup: 'FWRuleDDGroup' + } + ], + listeners: { + beforedrop: function(node, data, dropRec, dropPosition) { + if (!dropRec) { + return false; // empty view + } + var moveto = dropRec.get('pos'); + if (dropPosition === 'after') { + moveto++; + } + var pos = data.records[0].get('pos'); + me.moveRule(pos, moveto); + return 0; + }, + itemdblclick: run_editor + } + }, + sortableColumns: false, + columns: columns + }); + + me.callParent(); + + if (me.base_url) { + me.setBaseUrl(me.base_url); // load + } + } +}, function() { + + Ext.define('pve-fw-rule', { + extend: 'Ext.data.Model', + fields: [ { name: 'enable', type: 'boolean' }, + 'type', 'action', 'macro', 'source', 'dest', 'proto', 'iface', + 'dport', 'sport', 'comment', 'pos', 'digest', 'errors' ], + idProperty: 'pos' + }); + +});