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'