tripleo-ansible/tripleo_ansible/ansible_plugins/action/tripleo_iptables.py
Kevin Carter dcd18fa053
Add an alias for "port" when handling firewall rules
This change adds an internal alias for port to dport. This is done to
allow legacy config to operate as expected should a user have overrides
with the puppet legacy option. Should the action plugin encounter a
rule with the deprecated option a notice will be printed on screen
containing the rule and and information on how to convert it so that
functionality isn't lost on a future release.

Change-Id: I0643345a144a4b4c94c11465e9f8a82f13da146d
Signed-off-by: Kevin Carter <kecarter@redhat.com>
2021-01-27 11:56:46 -06:00

336 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
)
}
)
# NOTE(cloudnull): while dport is the only supported option,
# port has been added as an ailias to ensure
# our legacy configs remain functional.
if 'dport' in rule or 'port' in rule:
dport_rule_data = versioned_rule_data.copy()
dports = rule.get('dport', 'port')
if 'port' in rule:
DISPLAY.v(
'The use of "port" is deprecated and will be'
' removed in a future release. Please convert'
' all uses of "port" to "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