tripleo-ansible/tripleo_ansible/ansible_plugins/action/tripleo_iptables.py

325 lines
11 KiB
Python

# 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) <kecarter@redhat.com>
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
try:
from ansible_collections.ansible.netcommon.plugins.filter import ipaddr
except ImportError:
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'] = 'append'
rule_data['jump'] = rule.get('jump', 'DROP')
else:
rule_data['jump'] = rule.get('jump', 'ACCEPT')
rule_data['chain'] = rule.get('chain', 'INPUT')
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