From 434f2466b4af48f86acf70db55e071b6963df670 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Tue, 6 May 2014 12:27:00 +0200 Subject: [PATCH] add firewall GUI classes --- www/manager/dc/SecurityGroups.js | 230 +++++++++ www/manager/grid/FirewallAliases.js | 185 ++++++++ www/manager/grid/FirewallOptions.js | 227 +++++++++ www/manager/grid/FirewallRules.js | 699 ++++++++++++++++++++++++++++ www/manager/panel/Firewall.js | 78 ++++ www/manager/panel/IPSet.js | 426 +++++++++++++++++ www/manager/panel/SubConfigPanel.js | 80 ++++ 7 files changed, 1925 insertions(+) create mode 100644 www/manager/dc/SecurityGroups.js create mode 100644 www/manager/grid/FirewallAliases.js create mode 100644 www/manager/grid/FirewallOptions.js create mode 100644 www/manager/grid/FirewallRules.js create mode 100644 www/manager/panel/Firewall.js create mode 100644 www/manager/panel/IPSet.js create mode 100644 www/manager/panel/SubConfigPanel.js diff --git a/www/manager/dc/SecurityGroups.js b/www/manager/dc/SecurityGroups.js new file mode 100644 index 00000000..e2bf6e35 --- /dev/null +++ b/www/manager/dc/SecurityGroups.js @@ -0,0 +1,230 @@ +Ext.define('PVE.SecurityGroupEdit', { + extend: 'PVE.window.Edit', + + base_url: "/cluster/firewall/groups", + + allow_iface: false, + + initComponent : function() { + /*jslint confusion: true */ + var me = this; + + me.create = (me.group_name === undefined); + + var subject; + + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + + var items = [ + { + xtype: 'textfield', + name: 'group', + value: me.group_name || '', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: me.group_comment || '', + fieldLabel: gettext('Comment') + } + ]; + + if (me.create) { + subject = gettext('Security Group'); + } else { + subject = gettext('Security Group') + " '" + me.group_name + "'"; + items.push({ + xtype: 'hiddenfield', + name: 'rename', + value: me.group_name + }); + } + + var ipanel = Ext.create('PVE.panel.InputPanel', { + create: me.create, + items: items + }); + + + Ext.apply(me, { + subject: subject, + items: [ ipanel ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.SecurityGroupList', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveSecurityGroupList', + + rule_panel: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + base_url: "/cluster/firewall/groups", + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (me.rule_panel == undefined) { + throw "no rule panel specified"; + } + + if (me.base_url == undefined) { + throw "no base_url specified"; + } + + var store = new Ext.data.Store({ + fields: [ 'group', 'comment', 'digest' ], + proxy: { + type: 'pve', + url: '/api2/json' + me.base_url + }, + idProperty: 'group', + sorters: { + property: 'group', + order: 'DESC' + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + var oldrec = sm.getSelection()[0]; + store.load(function(records, operation, success) { + if (oldrec) { + var rec = store.findRecord('group', oldrec.data.group); + if (rec) { + sm.select(rec); + } + } + }); + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('PVE.SecurityGroupEdit', { + digest: rec.data.digest, + group_name: rec.data.group, + group_comment: rec.data.comment + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new PVE.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new PVE.button.Button({ + text: gettext('Create'), + handler: function() { + sm.deselectAll(); + var win = Ext.create('PVE.SecurityGroupEdit', {}); + win.show(); + win.on('destroy', reload); + } + }); + + me.removeBtn = new PVE.button.Button({ + text: gettext('Remove'), + selModel: sm, + disabled: true, + handler: function() { + var rec = sm.getSelection()[0]; + if (!rec || !me.base_url) { + return; + } + PVE.Utils.API2Request({ + url: me.base_url + '/' + rec.data.group + + '?digest=' + encodeURIComponent(rec.data.digest), + method: 'DELETE', + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: reload + }); + } + }); + + Ext.apply(me, { + store: store, + tbar: [ '' + gettext('Group') + ':', me.addBtn, me.removeBtn, me.editBtn ], + selModel: sm, + columns: [ + { header: gettext('Group'), dataIndex: 'group', width: 100 }, + { header: gettext('Comment'), dataIndex: 'comment', flex: 1 } + ], + listeners: { + itemdblclick: run_editor, + select: function(sm, rec) { + var url = '/cluster/firewall/groups/' + rec.data.group; + me.rule_panel.setBaseUrl(url); + }, + deselect: function() { + me.rule_panel.setBaseUrl(undefined); + }, + show: reload + } + }); + + me.callParent(); + + store.load(); + } +}); + +Ext.define('PVE.SecurityGroups', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveSecurityGroups', + + title: 'Security Groups', + + initComponent: function() { + var me = this; + + var rule_panel = Ext.createWidget('pveFirewallRules', { + region: 'center', + allow_groups: false, + tbar_prefix: '' + gettext('Rules') + ':', + flex: 0.75, + border: false + }); + + var sglist = Ext.createWidget('pveSecurityGroupList', { + region: 'west', + rule_panel: rule_panel, + flex: 0.25, + border: false, + split: true + }); + + + Ext.apply(me, { + layout: 'border', + items: [ sglist, rule_panel ], + listeners: { + show: function() { + sglist.fireEvent('show', sglist); + } + } + }); + + me.callParent(); + } +}); diff --git a/www/manager/grid/FirewallAliases.js b/www/manager/grid/FirewallAliases.js new file mode 100644 index 00000000..a307f1d6 --- /dev/null +++ b/www/manager/grid/FirewallAliases.js @@ -0,0 +1,185 @@ +Ext.define('PVE.FirewallAliasEdit', { + extend: 'PVE.window.Edit', + + base_url: undefined, + + alias_name: undefined, + + initComponent : function() { + /*jslint confusion: true */ + var me = this; + + me.create = (me.alias_name === undefined); + + if (me.create) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.alias_name; + me.method = 'PUT'; + } + + var items = [ + { + xtype: 'textfield', + name: me.create ? 'name' : 'rename', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'cidr', + fieldLabel: gettext('IP/CIDR'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + } + ]; + + var ipanel = Ext.create('PVE.panel.InputPanel', { + create: me.create, + items: items + }); + + Ext.apply(me, { + subject: gettext('Alias'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.create) { + me.load({ + success: function(response, options) { + var values = response.result.data; + values.rename = values.name; + ipanel.setValues(values); + } + }); + } + } +}); + +Ext.define('PVE.FirewallAliases', { + extend: 'Ext.grid.Panel', + alias: ['widget.pveFirewallAliases'], + + base_url: undefined, + + title: gettext('Aliases'), + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + if (!me.base_url) { + throw "missing base_url configuration"; + } + + var store = new Ext.data.Store({ + fields: [ 'name', 'cidr', 'comment', 'digest' ], + proxy: { + type: 'pve', + url: "/api2/json" + me.base_url + }, + idProperty: 'name', + sorters: { + property: 'name', + order: 'DESC' + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + var oldrec = sm.getSelection()[0]; + store.load(function(records, operation, success) { + if (oldrec) { + var rec = store.findRecord('name', oldrec.data.name); + if (rec) { + sm.select(rec); + } + } + }); + }; + + var run_editor = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.FirewallAliasEdit', { + base_url: me.base_url, + alias_name: rec.data.name + }); + + 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'), + handler: function() { + var win = Ext.create('PVE.FirewallAliasEdit', { + 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; + } + PVE.Utils.API2Request({ + url: me.base_url + '/' + rec.data.name, + method: 'DELETE', + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: reload + }); + } + }); + + + Ext.applyIf(me, { + store: store, + tbar: [ me.addBtn, me.removeBtn, me.editBtn ], + selModel: sm, + columns: [ + { header: gettext('Name'), dataIndex: 'name', width: 100 }, + { header: gettext('IP/CIDR'), dataIndex: 'cidr', width: 100 }, + { header: gettext('Comment'), dataIndex: 'comment', flex: 1 } + ], + listeners: { + itemdblclick: run_editor + } + }); + + me.callParent(); + + me.on('show', reload); + } +}); diff --git a/www/manager/grid/FirewallOptions.js b/www/manager/grid/FirewallOptions.js new file mode 100644 index 00000000..a3c117cb --- /dev/null +++ b/www/manager/grid/FirewallOptions.js @@ -0,0 +1,227 @@ +Ext.define('PVE.FirewallOptions', { + extend: 'PVE.grid.ObjectGrid', + alias: ['widget.pveFirewallOptions'], + + fwtype: undefined, // 'dc', 'node' or 'vm' + + base_url: undefined, + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + if (!me.base_url) { + throw "missing base_url configuration"; + } + + if (me.fwtype === 'dc' || me.fwtype === 'node' || me.fwtype === 'vm') { + if (me.fwtype === 'node') { + me.cwidth1 = 250; + } + } else { + throw "unknown firewall option type"; + } + + var rows = {}; + + var add_boolean_row = function(name, text, labelWidth) { + rows[name] = { + header: text, + required: true, + defaultValue: 0, + renderer: PVE.Utils.format_boolean, + editor: { + xtype: 'pveWindowEdit', + subject: text, + fieldDefaults: { labelWidth: labelWidth || 100 }, + items: { + xtype: 'pvecheckbox', + name: name, + uncheckedValue: 0, + fieldLabel: text + } + } + }; + }; + + var add_integer_row = function(name, text, labelWidth, minValue) { + rows[name] = { + header: text, + required: true, + renderer: function(value) { + return value || PVE.Utils.defaultText; + }, + editor: { + xtype: 'pveWindowEdit', + subject: text, + fieldDefaults: { labelWidth: labelWidth || 100 }, + items: { + xtype: 'numberfield', + name: name, + minValue: minValue, + decimalPrecision: 0, + fieldLabel: text, + emptyText: gettext('Default'), + getSubmitData: function() { + var me = this; + var val = me.getSubmitValue(); + if (val !== null && val !== '') { + var data = {}; + data[name] = val; + return data; + } else { + return { 'delete' : name }; + } + } + } + } + }; + }; + + var add_log_row = function(name, labelWidth) { + rows[name] = { + header: name, + required: true, + defaultValue: 'nolog', + editor: { + xtype: 'pveWindowEdit', + subject: name, + fieldDefaults: { labelWidth: labelWidth || 100 }, + items: { + xtype: 'pveKVComboBox', + name: name, + fieldLabel: name, + data: [['nolog', 'nolog'], ['info', 'info'], ['err', 'err'], + ['warning', 'warning'], ['crit', 'crit'], ['alert', 'alert'], + ['emerg', 'emerg'], ['debug', 'debug']] + } + } + }; + }; + + add_boolean_row('enable', gettext('Enable Firewall')); + + if (me.fwtype === 'node') { + add_boolean_row('nosmurfs', gettext('SMURFS filter')); + add_boolean_row('tcpflags', gettext('TCP flags filter')); + add_boolean_row('allow_bridge_route', gettext('Allow bridge route'), 150); + add_integer_row('nf_conntrack_max', 'nf_conntrack_max', 120, 32768); + add_integer_row('nf_conntrack_tcp_timeout_established', + 'nf_conntrack_tcp_timeout_established', 250, 7875); + add_log_row('log_level_in'); + add_log_row('log_level_out'); + add_log_row('tcp_flags_log_level', 120); + add_log_row('smurf_log_level'); + } else if (me.fwtype === 'vm') { + add_boolean_row('dhcp', gettext('Enable DHCP')); + add_boolean_row('macfilter', gettext('MAC filter')); + add_log_row('log_level_in'); + add_log_row('log_level_out'); + } + + if (me.fwtype === 'dc' || me.fwtype === 'vm') { + rows.policy_in = { + header: gettext('Input Policy'), + required: true, + defaultValue: 'DROP', + editor: { + xtype: 'pveWindowEdit', + subject: gettext('Input Policy'), + items: { + xtype: 'pveFirewallPolicySelector', + name: 'policy_in', + value: 'DROP', + fieldLabel: gettext('Input Policy') + } + } + }; + + rows.policy_out = { + header: gettext('Output Policy'), + required: true, + defaultValue: 'ACCEPT', + editor: { + xtype: 'pveWindowEdit', + subject: gettext('Output Policy'), + items: { + xtype: 'pveFirewallPolicySelector', + name: 'policy_out', + value: 'ACCEPT', + fieldLabel: gettext('Output Policy') + } + } + }; + } + + var reload = function() { + me.rstore.load(); + }; + + var run_editor = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rowdef = rows[rec.data.key]; + if (!rowdef.editor) { + return; + } + + var win; + if (Ext.isString(rowdef.editor)) { + win = Ext.create(rowdef.editor, { + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs' + me.base_url + }); + } else { + var config = Ext.apply({ + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs' + me.base_url + }, rowdef.editor); + win = Ext.createWidget(rowdef.editor.xtype, config); + win.load(); + } + + win.show(); + win.on('destroy', reload); + }; + + var edit_btn = new Ext.Button({ + text: gettext('Edit'), + disabled: true, + handler: run_editor + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + var rowdef = rows[rec.data.key]; + edit_btn.setDisabled(!rowdef.editor); + }; + + Ext.applyIf(me, { + url: "/api2/json" + me.base_url, + cwidth1: 150, + tbar: [ edit_btn ], + rows: rows, + listeners: { + itemdblclick: run_editor, + selectionchange: set_button_status + } + }); + + me.callParent(); + + me.on('show', reload); + } +}); diff --git a/www/manager/grid/FirewallRules.js b/www/manager/grid/FirewallRules.js new file mode 100644 index 00000000..281415b5 --- /dev/null +++ b/www/manager/grid/FirewallRules.js @@ -0,0 +1,699 @@ +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, + + initComponent : function() { + var me = this; + + me.column1 = [ + { + 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 + }, + { + xtype: 'pveKVComboBox', + name: 'proto', + value: '', + deleteEmpty: !me.create, + emptyText: 'any', + editable: true, + data: [['tcp', 'TCP'], ['udp', 'UDP'], ['icmp', 'ICMP']], + fieldLabel: gettext('Protocol'), + allowBlank: true + }, + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '' + }, + { + xtype: 'pveIPSetSelector', + name: 'source', + autoSelect: false, + editable: true, + queryDelay: 900000000, // disable query + value: '', + fieldLabel: gettext('Source') + }, + { + xtype: 'pveIPSetSelector', + name: 'dest', + autoSelect: false, + queryDelay: 900000000, // disable query + editable: true, + 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: '', + deleteEmpty: !me.create, + fieldLabel: gettext('Macro'), + allowBlank: true + } + ]; + + if (me.allow_iface) { + me.column2.push({ + xtype: 'pvetextfield', + name: 'iface', + deleteEmpty: !me.create, + value: '', + fieldLabel: gettext('Interface') + }); + } else { + me.column2.push({ + xtype: 'displayfield', + fieldLabel: '', + height: 22, // hack: set same height as text fields + value: '' + }); + } + + me.column2.push([ + { + 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, + + 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 ipanel = Ext.create('PVE.FirewallRulePanel', { + create: me.create, + 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); + } + }); + } + } +}); + +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, + + 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(); + } + }); + }, + + createRule: function(editor, rule) { + var me = this; + + if (!me.base_url) { + return; + } + + rule.pos = 0; + + rule.enable = rule.enable ? 1 : 0; + + PVE.Utils.API2Request({ + url: me.base_url, + method: 'POST', + params: rule, + waitMsgTarget: me, + failure: function(response, options) { + if (editor) { + editor.form.markInvalid(response.result.errors); + } else { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }, + callback: function() { + me.store.load(); + } + }); + }, + + updateRule: function(editor, rule) { + var me = this; + + if (!me.base_url) { + return; + } + + rule.enable = rule.enable ? 1 : 0; + + var pos = rule.pos; + delete rule.pos; + + PVE.Utils.API2Request({ + url: me.base_url + '/' + pos.toString(), + method: 'PUT', + params: rule, + waitMsgTarget: me, + failure: function(response, options) { + if (editor) { + editor.form.markInvalid(response.result.errors); + } else { + 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; + + 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, + 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 + }); + 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 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(undefined, data); + } + }, + width: 50 + }, + { + header: gettext('Type'), + dataIndex: 'type', + width: 50 + }, + { + header: gettext('Action'), + dataIndex: 'action', + width: 80 + }, + { + header: gettext('Macro'), + dataIndex: 'macro', + width: 80 + } + ]; + + if (me.allow_iface) { + columns.push({ + header: gettext('Interface'), + dataIndex: 'iface', + width: 80 + }); + } + + columns.push([ + { + header: gettext('Source'), + dataIndex: 'source', + width: 100 + }, + { + header: gettext('Destination'), + dataIndex: 'dest', + width: 100 + }, + { + header: gettext('Protocol'), + dataIndex: 'proto', + width: 100 + }, + { + header: gettext('Dest. port'), + dataIndex: 'dport', + width: 100 + }, + { + header: gettext('Source port'), + dataIndex: 'sport', + width: 100 + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + flex: 1, + renderer: function(value) { + return Ext.util.Format.htmlEncode(value); + } + } + ]); + + 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 + } + }, + 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' ], + idProperty: 'pos' + }); + +}); diff --git a/www/manager/panel/Firewall.js b/www/manager/panel/Firewall.js new file mode 100644 index 00000000..02d28fc1 --- /dev/null +++ b/www/manager/panel/Firewall.js @@ -0,0 +1,78 @@ +Ext.define('PVE.panel.Firewall', { + extend: 'PVE.panel.SubConfig', + alias: 'widget.pveFirewallPanel', + + configPrefix: 'firewall', + + fwtype: undefined, // 'dc', 'node' or 'vm' + + base_url: undefined, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.base_url) { + throw "no base_url specified"; + } + + if (!(me.fwtype === 'dc' || me.fwtype === 'node' || me.fwtype === 'vm')) { + throw "unknown firewall panel type"; + } + + var items = [ + { + xtype: 'pveFirewallRules', + title: 'Rules', + allow_iface: true, + base_url: me.base_url + '/rules', + itemId: 'rules' + } + ]; + + if (me.fwtype === 'dc') { + items.push({ + xtype: 'pveSecurityGroups', + title: 'Security Groups', + itemId: 'sg' + }); + items.push({ + xtype: 'pveFirewallAliases', + base_url: '/cluster/firewall/aliases', + itemId: 'aliases' + }); + items.push({ + xtype: 'pveIPSet', + base_url: '/cluster/firewall/ipset', + itemId: 'ipset' + }); + } + + items.push({ + xtype: 'pveFirewallOptions', + title: 'Options', + base_url: me.base_url + '/options', + fwtype: me.fwtype, + itemId: 'options' + }); + + if (me.fwtype !== 'dc') { + items.push({ + title: 'Log', + itemId: 'fwlog', + xtype: 'pveLogView', + url: '/api2/extjs' + me.base_url + '/log' + }); + } + + Ext.apply(me, { + defaults: { + border: false, + pveSelNode: me.pveSelNode + }, + items: items + }); + + me.callParent(); + } +}); \ No newline at end of file diff --git a/www/manager/panel/IPSet.js b/www/manager/panel/IPSet.js new file mode 100644 index 00000000..afd55b83 --- /dev/null +++ b/www/manager/panel/IPSet.js @@ -0,0 +1,426 @@ +Ext.define('PVE.IPSetList', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveIPSetList', + + ipset_panel: undefined, + + base_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (me.ipset_panel == undefined) { + throw "no rule panel specified"; + } + + if (me.base_url == undefined) { + throw "no base_url specified"; + } + + var store = new Ext.data.Store({ + fields: [ 'name', 'comment', 'digest' ], + proxy: { + type: 'pve', + url: "/api2/json" + me.base_url + }, + idProperty: 'name', + sorters: { + property: 'name', + order: 'DESC' + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + var oldrec = sm.getSelection()[0]; + store.load(function(records, operation, success) { + if (oldrec) { + var rec = store.findRecord('name', oldrec.data.name); + if (rec) { + sm.select(rec); + } + } + }); + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('PVE.window.Edit', { + subject: "IPSet '" + rec.data.name + "'", + url: me.base_url, + method: 'POST', + digest: rec.data.digest, + items: [ + { + xtype: 'hiddenfield', + name: 'rename', + value: rec.data.name + }, + { + xtype: 'textfield', + name: 'name', + value: rec.data.name, + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: rec.data.comment, + fieldLabel: gettext('Comment') + } + ] + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new PVE.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new PVE.button.Button({ + text: gettext('Create'), + handler: function() { + sm.deselectAll(); + var win = Ext.create('PVE.window.Edit', { + subject: 'IPSet', + url: me.base_url, + method: 'POST', + items: [ + { + xtype: 'textfield', + name: 'name', + value: '', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + win.show(); + win.on('destroy', reload); + + } + }); + + me.removeBtn = new PVE.button.Button({ + text: gettext('Remove'), + selModel: sm, + disabled: true, + handler: function() { + var rec = sm.getSelection()[0]; + if (!rec || !me.base_url) { + return; + } + PVE.Utils.API2Request({ + url: me.base_url + '/' + rec.data.name, + method: 'DELETE', + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: reload + }); + } + }); + + Ext.apply(me, { + store: store, + tbar: [ 'IPSet:', me.addBtn, me.removeBtn, me.editBtn ], + selModel: sm, + columns: [ + { header: 'IPSet', dataIndex: 'name', width: 100 }, + { header: gettext('Comment'), dataIndex: 'comment', flex: 1 } + ], + listeners: { + itemdblclick: run_editor, + select: function(sm, rec) { + var url = me.base_url + '/' + rec.data.name; + me.ipset_panel.setBaseUrl(url); + }, + deselect: function() { + me.ipset_panel.setBaseUrl(undefined); + }, + show: reload + } + }); + + me.callParent(); + + store.load(); + } +}); + +Ext.define('PVE.IPSetCidrEdit', { + extend: 'PVE.window.Edit', + + cidr: undefined, + + initComponent : function() { + /*jslint confusion: true */ + var me = this; + + me.create = (me.cidr === undefined); + + if (me.create) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.cidr; + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.panel.InputPanel', { + create: me.create, + column1: [ + { + xtype: me.create ? 'textfield' : 'displayfield', + name: 'cidr', + height: 22, // hack: set same height as text fields + value: '', + fieldLabel: gettext('IP/CIDR') + } + ], + column2: [ + { + xtype: 'pvecheckbox', + name: 'nomatch', + checked: false, + height: 22, // hack: set same height as text fields + uncheckedValue: 0, + fieldLabel: gettext('nomatch') + } + ], + columnB: [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + + Ext.apply(me, { + subject: gettext('IP/CIDR'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.create) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + } + }); + } + } +}); + +Ext.define('PVE.IPSetGrid', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveIPSetGrid', + + base_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + setBaseUrl: function(url) { + var me = this; + + me.base_url = url; + + if (url === undefined) { + me.addBtn.setDisabled(true); + me.store.removeAll(); + } else { + me.addBtn.setDisabled(false); + me.store.setProxy({ + type: 'pve', + url: '/api2/json' + url + }); + + me.store.load(); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-ipset' + }); + + 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 win = Ext.create('PVE.IPSetCidrEdit', { + base_url: me.base_url, + cidr: rec.data.cidr + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new PVE.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new PVE.button.Button({ + text: gettext('Add'), + disabled: true, + handler: function() { + if (!me.base_url) { + return; + } + var win = Ext.create('PVE.IPSetCidrEdit', { + base_url: me.base_url + }); + win.show(); + win.on('destroy', reload); + } + }); + + me.removeBtn = new PVE.button.Button({ + text: gettext('Remove'), + selModel: sm, + disabled: true, + handler: function() { + var rec = sm.getSelection()[0]; + if (!rec || !me.base_url) { + return; + } + + PVE.Utils.API2Request({ + url: me.base_url + '/' + rec.data.cidr, + method: 'DELETE', + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: reload + }); + } + }); + + Ext.apply(me, { + tbar: [ 'IP/CIDR:', me.addBtn, me.removeBtn, me.editBtn ], + store: store, + selModel: sm, + listeners: { + itemdblclick: run_editor + }, + columns: [ + { + xtype: 'rownumberer' + }, + { + header: gettext('IP/CIDR'), + dataIndex: 'cidr', + width: 150, + renderer: function(value, metaData, record) { + if (record.data.nomatch) { + return '! ' + value; + } + return value; + } + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + flex: 1, + renderer: function(value) { + return Ext.util.Format.htmlEncode(value); + } + } + ] + }); + + me.callParent(); + + if (me.base_url) { + me.setBaseUrl(me.base_url); // load + } + } +}, function() { + + Ext.define('pve-ipset', { + extend: 'Ext.data.Model', + fields: [ { name: 'nomatch', type: 'boolean' }, + 'cidr', 'comment' ], + idProperty: 'cidr' + }); + +}); + +Ext.define('PVE.IPSet', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveIPSet', + + title: 'IPSet', + + initComponent: function() { + var me = this; + + var ipset_panel = Ext.createWidget('pveIPSetGrid', { + region: 'center', + flex: 0.5, + border: false + }); + + var ipset_list = Ext.createWidget('pveIPSetList', { + region: 'west', + ipset_panel: ipset_panel, + base_url: me.base_url, + flex: 0.5, + border: false, + split: true + }); + + Ext.apply(me, { + layout: 'border', + items: [ ipset_list, ipset_panel ], + listeners: { + show: function() { + ipset_list.fireEvent('show', ipset_list); + } + } + }); + + me.callParent(); + } +}); diff --git a/www/manager/panel/SubConfigPanel.js b/www/manager/panel/SubConfigPanel.js new file mode 100644 index 00000000..849fd041 --- /dev/null +++ b/www/manager/panel/SubConfigPanel.js @@ -0,0 +1,80 @@ +Ext.define('PVE.panel.SubConfig', { + extend: 'Ext.tab.Panel', + alias: ['widget.pvePanelSubConfig'], + + configPrefix: undefined, + + getHState: function(itemId) { + /*jslint confusion: true */ + var me = this; + + if (!itemId) { + itemId = me.getActiveTab().itemId; + } + + var first = me.items.get(0); + var ntab; + + // Note: '' is alias for first tab. + if (itemId === first.itemId) { + ntab = me.configPrefix; + } else { + ntab = me.configPrefix + '-' + itemId; + } + + return { value: ntab }; + }, + + initComponent: function() { + var me = this; + + if (!me.phstateid) { + throw "no parent history state specified"; + } + + var sp = Ext.state.Manager.getProvider(); + var state = sp.get(me.phstateid); + + var hsregex = /^([^\-\s]+)-(\S+)?$/; + + if (state && state.value) { + var res = hsregex.exec(state.value); + if (res && res[1] && res[2] && res[1] === me.configPrefix) { + me.activeTab = res[2]; + } + } + + Ext.apply(me, { + plain: true, + tabPosition: 'bottom', + listeners: { + afterrender: function(tp) { + var first = tp.items.get(0); + if (first) { + first.fireEvent('show', first); + } + }, + tabchange: function(tp, newcard, oldcard) { + var state = me.getHState(newcard.itemId); + sp.set(me.phstateid, state); + } + } + }); + + me.callParent(); + + var statechange = function(sp, key, state) { + if ((key === me.phstateid) && state) { + var first = me.items.get(0); + var atab = me.getActiveTab().itemId; + var res = hsregex.exec(state.value); + var ntab = (res && res[1]) ? res[1] : first.itemId; + if (ntab && (atab != ntab)) { + me.setActiveTab(ntab); + } + } + }; + + me.mon(sp, 'statechange', statechange); + } +});