From 6b943357fcf53419ce8b071d7bea66f583cf881d Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Wed, 11 Dec 2019 20:02:49 -0600 Subject: [PATCH] Update firewall role to use an action plugin This change updates our firewall role so that its using an action plugin to invoke the iptables module. This is being done to speed up rule creation and maintenance. Tests have been updated to ensure we're capturing all of the logic within the action plugin accordingly. Depends-On: Ie9b6fd5792efb270ae577b08d6a2d5b78dabe5e7 Closes-Bug: #1856094 Change-Id: I3e4c6586796753b5d1cb9aa6a7c3eee6ecc235fb Signed-off-by: Kevin Carter (cherry picked from commit 38c75fb83e059b44e7344eca93a8fdadee04630f) --- .../action/tripleo_iptables.py | 320 ++++++++++++++++++ .../firewall-add-complex/playbook.yml | 8 + .../firewall-remove-complex/playbook.yml | 10 + .../roles/tripleo-firewall/tasks/main.yml | 20 +- .../tasks/tripleo_firewall_add.yml | 156 --------- .../tasks/tripleo_firewall_state.yml | 28 -- 6 files changed, 343 insertions(+), 199 deletions(-) create mode 100644 tripleo_ansible/ansible_plugins/action/tripleo_iptables.py delete mode 100644 tripleo_ansible/roles/tripleo-firewall/tasks/tripleo_firewall_add.yml delete mode 100644 tripleo_ansible/roles/tripleo-firewall/tasks/tripleo_firewall_state.yml diff --git a/tripleo_ansible/ansible_plugins/action/tripleo_iptables.py b/tripleo_ansible/ansible_plugins/action/tripleo_iptables.py new file mode 100644 index 000000000..7cd75df71 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/action/tripleo_iptables.py @@ -0,0 +1,320 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +DOCUMENTATION = """ +--- +module: tripleo_iptables +author: + - Kevin Carter (@cloudnull) +version_added: '2.8' +short_description: Runs iptables module commands in bulk. +notes: [] +description: + - This module accepts iptables rules in list format and batches their + creation to speed up the creation of rules at scale. +options: + tripleo_rules: + description: + - List of rules to batch, rules have been constructed using the tripleo + spec and will be formatted to match the input values of the core + iptables module. + required: True +""" + +EXAMPLES = """ +- name: Run Package Installation + tripleo_iptables: + tripleo_rules: + - '1 rule special': + dport: + - 1234 + - 4321 + - '2 rule special also': + dport: + - 2345 + - 5432 +""" + + +from ansible.plugins.action import ActionBase +from ansible.plugins.filter import ipaddr +from ansible.utils.display import Display + + +DISPLAY = Display() +RULE_STATES = { + 'enabled': 'present', + 'present': 'present', + 'absent': 'absent', + 'disabled': 'absent' +} +IPTABLES_BIN = { + 'ipv4': 'iptables', + 'ipv6': 'ip6tables' +} +IPTABLES_CHAIN_CMD = """ +if ! {cmd} --list "{chain}"; then + {cmd} -N "{chain}" +fi +""" +IPTABLES_CHAINS = ('INPUT', 'OUTPUT', 'FORWARD') + + +class ActionModule(ActionBase): + """Batch iptables rules for faster rule creation.""" + + def _run_module(self, name, args, task_vars): + """Runs an ansible module and collects return information. + + :returns: boolean + """ + + module_return = self._execute_module( + module_name=name, + module_args=args, + task_vars=task_vars + ) + changed = module_return.get('changed') + if changed: + self.return_data['changed'] = True + + self.return_data['stdout'] = module_return.get('stdout') + self.return_data['stderr'] = module_return.get('stderr') + self.return_data['msg'] = module_return.get('msg') + self.return_data['cmd'] = module_return.get('cmd') + self.return_data['rc'] = module_return.get('rc', 0) + fatal = self.return_data['failed'] = module_return.get( + 'failed', + False + ) + DISPLAY.vv('Module name: {}'.format(name)) + DISPLAY.vv('Module args: {}'.format(args)) + if fatal: + DISPLAY.error('Failed, module return: {}'.format(module_return)) + DISPLAY.error('Failed, return data: {}'.format(self.return_data)) + + return fatal + + @staticmethod + def _check_rule_data(rule_data, ipversion): + """Check the rule data for compatible ip version information. + + This function uses the ansible ipaddr filter to validate IP + information when a source or destination has been provided. + + :returns: boolean + """ + + kwargs_hash = { + 'ipv6': { + 'version': 6, + 'query': 'ipv6', + 'alias': 'ipv6' + }, + 'ipv4': { + 'version': 4, + 'query': 'ipv4', + 'alias': 'ipv4' + } + } + + for arg in ('source', 'destination'): + ip_data = rule_data.get(arg) + if ip_data: + DISPLAY.v( + 'Checking "{}" against "{}" with ip version "{}"'.format( + arg, + ip_data, + ipversion + ) + ) + ip_data_check = ipaddr.ipaddr( + value=ip_data, + **kwargs_hash[ipversion] + ) + DISPLAY.vvv('ipaddr filter return "{}"'.format(ip_data_check)) + if not ip_data_check: + DISPLAY.v( + 'Rule has a "{}" but the value "{}" is not applicable' + ' to ip version "{}"'.format( + arg, + ip_data, + ipversion + + ) + ) + DISPLAY.vvv('Rule data: "{}"'.format(rule_data)) + return False + else: + return True + + def queue_rules(self): + """Add chains and rules to the required queues.""" + + for item in self._task.args['tripleo_rules']: + rule_data = dict() + rule = item['rule'] + + ipversions = rule.get('ipversion', ['ipv4', 'ipv6']) + if not isinstance(ipversions, list): + ipversions = [ipversions] + + state = rule.get('extras', dict()).get('ensure', 'enabled') + rule_data['state'] = RULE_STATES[state] + + action = rule_data['action'] = rule.get('action', 'insert') + if action == 'drop': + rule_data['action'] = 'insert' + rule_data['state'] = 'absent' + + rule_data['chain'] = rule.get('chain', 'INPUT') + rule_data['jump'] = rule.get('jump', 'ACCEPT') + rule_data['protocol'] = rule.get('proto', 'tcp') + if 'table' in rule: + rule_data['table'] = rule['table'] + + if 'interface' in rule: + rule_data['in_interface'] = rule['interface'] + + if 'sport' in rule: + rule_data['source_port'] = rule['sport'] + + if 'source' in rule: + rule_data['source'] = rule['source'] + + if rule_data['protocol'] != 'gre': + rule_data['ctstate'] = rule.get('state', 'NEW') + + if 'limit' in rule: + rule_data['limit'] = rule['limit'] + + if 'limit_burst' in rule: + rule_data['limit_burst'] = rule['limit_burst'] + + if 'destination' in rule: + rule_data['destination'] = rule['destination'] + + for ipversion in ipversions: + if not self._check_rule_data(rule_data=rule_data, + ipversion=ipversion): + continue + + versioned_rule_data = rule_data.copy() + versioned_rule_data['ip_version'] = ipversion + if 'rule_name' in item: + versioned_rule_data['comment'] = '{} {}'.format( + item['rule_name'], + ipversion + ) + + if not versioned_rule_data['chain'] in IPTABLES_CHAINS: + chain = versioned_rule_data['chain'] + DISPLAY.v( + 'Queueing chain: {}, ip version {}'.format( + chain, ipversion + ) + ) + self.iptables_chains.append( + { + 'ipv': ipversion, + 'chain': chain, + 'command': IPTABLES_CHAIN_CMD.format( + cmd=IPTABLES_BIN[ipversion], + chain=chain + ) + } + ) + + if 'dport' in rule: + dport_rule_data = versioned_rule_data.copy() + dports = rule['dport'] + if not isinstance(dports, list): + dports = [dports] + + for dport in dports: + if isinstance(dport, int): + dport_rule_data['destination_port'] = dport + else: + dport = dport.replace('-', ':') + dport_rule_data['destination_port'] = dport + + DISPLAY.v( + 'Queueing port rule: {},' + ' ip version: {},' + ' dport: {}'.format( + dport_rule_data.get('comment', None), + ipversion, + dport_rule_data['destination_port'] + ) + ) + self.iptables_rules.append(dport_rule_data.copy()) + else: + DISPLAY.v( + 'Queueing service rule: {},' + ' ip version: {}'.format( + versioned_rule_data.get('comment', None), + ipversion + ) + ) + self.iptables_rules.append(versioned_rule_data.copy()) + + def run(self, tmp=None, task_vars=None): + """Run the iptables firewall rule batcher. + + When rules are batched, the chains will be created before the rules. + """ + + self.return_data = dict() + self.iptables_rules = list() + self.iptables_chains = list() + + self.queue_rules() + + for iptables_chain in self.iptables_chains: + DISPLAY.v( + 'Managing chain: {} for version {}'.format( + iptables_chain['chain'], + iptables_chain['ipv'] + ) + ) + return_data = self._low_level_execute_command( + iptables_chain['command'], + executable='/bin/bash' + ) + if return_data['rc'] > 0: + DISPLAY.error(msg='Failed command: {}'.format(iptables_chain)) + DISPLAY.error(msg='Failed chain data: {}'.format(return_data)) + return return_data + + for iptables_rule in self.iptables_rules: + DISPLAY.v( + 'Managing rule: {},' + ' dport: {},' + ' ip version: {}'.format( + iptables_rule.get('comment', 'undefined'), + iptables_rule.get('destination_port', 'undefined'), + iptables_rule['ip_version'], + ) + ) + fatal = self._run_module( + name='iptables', + args=iptables_rule, + task_vars=task_vars + ) + if fatal: + return self.return_data + + return self.return_data diff --git a/tripleo_ansible/roles/tripleo-firewall/molecule/firewall-add-complex/playbook.yml b/tripleo_ansible/roles/tripleo-firewall/molecule/firewall-add-complex/playbook.yml index 69b03be79..ac9580444 100644 --- a/tripleo_ansible/roles/tripleo-firewall/molecule/firewall-add-complex/playbook.yml +++ b/tripleo_ansible/roles/tripleo-firewall/molecule/firewall-add-complex/playbook.yml @@ -47,3 +47,11 @@ dport: 2211 '006 ironic-inspector': dport: 2212 + '124 snmp': + dport: 2212 + source: '192.168.24.1/24' + chain: test-chain2 + '125 snmp': + dport: 2212 + destination: '::' + chain: test-chain2 diff --git a/tripleo_ansible/roles/tripleo-firewall/molecule/firewall-remove-complex/playbook.yml b/tripleo_ansible/roles/tripleo-firewall/molecule/firewall-remove-complex/playbook.yml index d5d6e1762..4352f48e2 100644 --- a/tripleo_ansible/roles/tripleo-firewall/molecule/firewall-remove-complex/playbook.yml +++ b/tripleo_ansible/roles/tripleo-firewall/molecule/firewall-remove-complex/playbook.yml @@ -61,3 +61,13 @@ dport: 2212 extras: ensure: 'absent' + '124 snmp': + dport: 2212 + source: '192.168.24.1/24' + extras: + ensure: 'absent' + '125 snmp': + dport: 2212 + destination: '::' + extras: + ensure: 'absent' diff --git a/tripleo_ansible/roles/tripleo-firewall/tasks/main.yml b/tripleo_ansible/roles/tripleo-firewall/tasks/main.yml index 42ad60610..d7f6c7270 100644 --- a/tripleo_ansible/roles/tripleo-firewall/tasks/main.yml +++ b/tripleo_ansible/roles/tripleo-firewall/tasks/main.yml @@ -39,18 +39,6 @@ list }}" -- name: Check rule set - fail: - msg: >- - `{{ item['rule_name'] }}` firewall rule cannot be created. TCP or UDP rules - for INPUT or OUTPUT need sport or dport defined. - when: - - ((item['rule']['proto'] | default('tcp')) in ['tcp', 'udp']) and - (item['rule']['dport'] is undefined) and - ((item['rule']['chain'] | default('INPUT')) != 'FORWARD') and - ((item['rule']['table'] | default('filter')) != 'nat') - loop: "{{ firewall_rules_sorted }}" - - name: Firewall add block become: true block: @@ -65,6 +53,8 @@ state: started enabled: true - - name: Enable filewall port config - include_tasks: tripleo_firewall_add.yml - loop: "{{ firewall_rules_sorted }}" + - name: Manage firewall rules + tripleo_iptables: + tripleo_rules: "{{ firewall_rules_sorted }}" + notify: + - Save firewall rules diff --git a/tripleo_ansible/roles/tripleo-firewall/tasks/tripleo_firewall_add.yml b/tripleo_ansible/roles/tripleo-firewall/tasks/tripleo_firewall_add.yml deleted file mode 100644 index adf68959c..000000000 --- a/tripleo_ansible/roles/tripleo-firewall/tasks/tripleo_firewall_add.yml +++ /dev/null @@ -1,156 +0,0 @@ ---- -# Copyright 2019 Red Hat, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -# NOTE(Cloudnull): This task exists because the iptables module will not -# create a chain. There is a feature request open for this -# [ https://github.com/ansible/ansible/issues/25099 ]. -# A change has been added to support this functionality but -# it is awaiting review and merge. -# [ https://github.com/ansible/ansible/pull/32158 ]. When -# this change is merged this task should be removed. -- name: Ensure chains exist - shell: |- - EXIT_CODE=0 - if ! iptables --list "{{ item['rule']['chain'] }}"; then - iptables -N "{{ item['rule']['chain'] }}" - EXIT_CODE=99 - fi - if ! ip6tables --list "{{ item['rule']['chain'] }}"; then - ip6tables -N "{{ item['rule']['chain'] }}" - EXIT_CODE=99 - fi - exit ${EXIT_CODE} - when: - - (item['rule']['chain'] | default('INPUT')) != 'INPUT' - - tripleo_firewall_port_states[(item['rule']['extras'] | default({}))['ensure'] | default('enabled')] == "present" - register: iptables_chain - changed_when: iptables_chain.rc == 99 - failed_when: not (iptables_chain.rc in [0, 99]) - -- include_tasks: tripleo_firewall_state.yml - -# NOTE(Cloudnull): This task adds multiport rules using a loop instead of using -# the multiport key word. While multiport is perfectly functional -# using raw iptables rules, it is not supported in the ansible -# module. The use of the loop will be revised just as soon as the -# pull request [ https://github.com/ansible/ansible/pull/21071 ] -# is merged. -- name: Firewall port rule (ipv4) - iptables: - action: insert - table: "{{ item['rule']['table'] | default(omit) }}" - chain: "{{ item['rule']['chain'] | default('INPUT') }}" - in_interface: "{{ item['rule']['interface'] | default(omit) }}" - protocol: "{{ item['rule']['proto'] | default('tcp') }}" - destination_port: "{{ port | replace('-', ':') }}" - destination: "{{ item['rule']['destination'] | default(omit) }}" - source_port: "{{ item['rule']['sport'] | default(omit) | replace('-', ':') }}" - source: "{{ item['rule']['source'] | default(omit) }}" - comment: "{{ item['rule_name'] }} ipv4" - jump: "{{ item['rule']['jump'] | default('ACCEPT') }}" - ctstate: "{{ tripleo_ctstate }}" - limit: "{{ item['rule']['limit'] | default(omit) }}" - limit_burst: "{{ item['rule']['limit_burst'] | default(omit) }}" - ip_version: ipv4 - state: "{{ tripleo_firewall_port_states[(item['rule']['extras'] | default({}))['ensure'] | default('enabled')] }}" - when: - - item['rule']['dport'] is defined - - (item['rule']['ipversion'] | default('ipv4')) != 'ipv6' - - item['rule']['source'] | default('127.0.0.1') | ipv4 - - item['rule']['destination'] | default('127.0.0.1') | ipv4 - loop: "{{ ((item['rule']['dport'] is iterable) and (item['rule']['dport'] is not string)) | ternary(item['rule']['dport'], [item['rule']['dport']]) }}" - loop_control: - loop_var: port - notify: - - Save firewall rules - -# NOTE(Cloudnull): This task adds multiport rules using a loop instead of using -# the multiport key word. While multiport is perfectly functional -# using raw iptables rules, it is not supported in the ansible -# module. The use of the loop will be revised just as soon as the -# pull request [ https://github.com/ansible/ansible/pull/21071 ] -# is merged. -- name: Firewall port rule (ipv6) - iptables: - action: insert - table: "{{ item['rule']['table'] | default(omit) }}" - chain: "{{ item['rule']['chain'] | default('INPUT') }}" - in_interface: "{{ item['rule']['interface'] | default(omit) }}" - protocol: "{{ item['rule']['proto'] | default('tcp') }}" - destination_port: "{{ port | replace('-', ':') }}" - destination: "{{ item['rule']['destination'] | default(omit) }}" - source_port: "{{ item['rule']['sport'] | default(omit) | replace('-', ':') }}" - source: "{{ item['rule']['source'] | default(omit) }}" - comment: "{{ item['rule_name'] }} ipv6" - jump: "{{ item['rule']['jump'] | default('ACCEPT') }}" - ctstate: "{{ tripleo_ctstate }}" - limit: "{{ item['rule']['limit'] | default(omit) }}" - limit_burst: "{{ item['rule']['limit_burst'] | default(omit) }}" - ip_version: ipv6 - state: "{{ tripleo_firewall_port_states[(item['rule']['extras'] | default({}))['ensure'] | default('enabled')] }}" - when: - - item['rule']['dport'] is defined - - (item['rule']['ipversion'] | default('ipv6')) != 'ipv4' - - item['rule']['source'] | default('::') | ipv6 - - item['rule']['destination'] | default('::') | ipv6 - loop: "{{ ((item['rule']['dport'] is iterable) and (item['rule']['dport'] is not string)) | ternary(item['rule']['dport'], [item['rule']['dport']]) }}" - loop_control: - loop_var: port - notify: - - Save firewall rules - -- name: Firewall protocol rule (ipv4) - iptables: - action: insert - table: "{{ item['rule']['table'] | default(omit) }}" - chain: "{{ item['rule']['chain'] | default('INPUT') }}" - in_interface: "{{ item['rule']['interface'] | default(omit) }}" - protocol: "{{ item['rule']['proto'] | default(omit) }}" - source_port: "{{ item['rule']['sport'] | default(omit) | replace('-', ':') }}" - source: "{{ item['rule']['source'] | default(omit) }}" - comment: "{{ item['rule_name'] }} ipv4" - jump: "{{ item['rule']['jump'] | default('ACCEPT') }}" - ctstate: "{{ tripleo_ctstate }}" - limit: "{{ item['rule']['limit'] | default(omit) }}" - limit_burst: "{{ item['rule']['limit_burst'] | default(omit) }}" - ip_version: ipv4 - state: "{{ tripleo_firewall_port_states[(item['rule']['extras'] | default({}))['ensure'] | default('enabled')] }}" - when: - - (item['rule']['ipversion'] | default('ipv4')) != 'ipv6' - - item['rule']['proto'] is defined - - item['rule']['dport'] is undefined - -- name: Firewall protocol rule (ipv6) - iptables: - action: insert - table: "{{ item['rule']['table'] | default(omit) }}" - chain: "{{ item['rule']['chain'] | default('INPUT') }}" - in_interface: "{{ item['rule']['interface'] | default(omit) }}" - protocol: "{{ item['rule']['proto'] | default(omit) }}" - source_port: "{{ item['rule']['sport'] | default(omit) | replace('-', ':') }}" - source: "{{ item['rule']['source'] | default(omit) }}" - comment: "{{ item['rule_name'] }} ipv6" - jump: "{{ item['rule']['jump'] | default('ACCEPT') }}" - ctstate: "{{ tripleo_ctstate }}" - limit: "{{ item['rule']['limit'] | default(omit) }}" - limit_burst: "{{ item['rule']['limit_burst'] | default(omit) }}" - ip_version: ipv6 - state: "{{ tripleo_firewall_port_states[(item['rule']['extras'] | default({}))['ensure'] | default('enabled')] }}" - when: - - (item['rule']['ipversion'] | default('ipv6')) != 'ipv4' - - item['rule']['proto'] is defined - - item['rule']['dport'] is undefined diff --git a/tripleo_ansible/roles/tripleo-firewall/tasks/tripleo_firewall_state.yml b/tripleo_ansible/roles/tripleo-firewall/tasks/tripleo_firewall_state.yml deleted file mode 100644 index b8869d86d..000000000 --- a/tripleo_ansible/roles/tripleo-firewall/tasks/tripleo_firewall_state.yml +++ /dev/null @@ -1,28 +0,0 @@ ---- -# Copyright 2019 Red Hat, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -- name: Set gre state fact - set_fact: - tripleo_ctstate: [] - when: - - (item['rule']['proto'] | default('tcp')) == 'gre' - -- name: Set general state fact - set_fact: - tripleo_ctstate: "{{ item['rule']['ctstate'] | default(item['rule']['state'] | default('NEW')) }}" - when: - - (item['rule']['proto'] | default('tcp')) != 'gre'