dcd18fa053
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>
336 lines
11 KiB
Python
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
|