mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
New module: iptables_state (#271)
* restart from last state
* test (sanity) doc fragment placeholder
* test (sanity) remove doc fragment placeholder
* remove internal params from DOCUMENTATION
* update ignore-2.10.txt
* doc: add changelog fragment
* shorten changelog fragment
* Revert "shorten changelog fragment"
This reverts commit f9aea0d1eaefda139fd5b79bd0eb127c09a433fb.
* test with posix/group1
* test with posix/group3
* test with posix/group5
* test with posix/group4
* test with posix/group3
* New modules/action plugins automatically get a changelog entry
* fix: styles
* Revert "remove internal params from DOCUMENTATION"
This reverts commit 7d5fcf4b17e4cd5b0afc08fd1bd3fcef5fcaee26.
* drop neutral/informative/stateless behaviour
* update tasks after changes in module
* use FQCN in EXAMPLES
* add tests to validate error handling about required params
* doc: remove outdated sentence
* do not document internal parameters
* display timeout value in failure message
* remove inapropriate comment
* merge results and clean them up only once
* conditionally remove tmp path
* at least one iteration is required
* remove deprecated code
* move variables declaration to conditional block
* dissociate async and connection timeout
* improve warnings (conditions + values)
* remove ANSIBLE_METADATA (no more needed); fix typo
* update DOCUMENTATION
* Drop field 'version_added' (no more needed).
* Add a note about check_mode support.
* catch early errors before resetting connection and processing the loop
* fix typo
* change posix group (due to xtables locks); add 'version_added' in doc
* update deprecation (replace Ansible 2.12 by community.general 2.0.0)
* bump version_added to 1.0.0
* update ignore-2.11.txt
* ignore errors for 2.9 as for 2.10 & 2.11
* move action plugin to system/ and replace it by a symlink
* remove action-plugin-docs override in tests/sanity/ignore*.txt
* update action plugin docstrings
* bump version_added to 1.1.0
* use lowercase booleans
* extend usage of namespaces to ansible builtin modules
(cherry picked from commit 92242d898d
)
This commit is contained in:
parent
fe3e262209
commit
9226c4b0d5
14 changed files with 1682 additions and 0 deletions
0
plugins/action/__init__.py
Normal file
0
plugins/action/__init__.py
Normal file
1
plugins/action/iptables_state.py
Symbolic link
1
plugins/action/iptables_state.py
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
./system/iptables_state.py
|
189
plugins/action/system/iptables_state.py
Normal file
189
plugins/action/system/iptables_state.py
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
# Copyright: (c) 2020, quidame <quidame@poivron.org>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ansible.plugins.action import ActionBase
|
||||||
|
from ansible.errors import AnsibleError, AnsibleActionFail, AnsibleConnectionFailure
|
||||||
|
from ansible.utils.vars import merge_hash
|
||||||
|
from ansible.utils.display import Display
|
||||||
|
|
||||||
|
display = Display()
|
||||||
|
|
||||||
|
|
||||||
|
class ActionModule(ActionBase):
|
||||||
|
|
||||||
|
# Keep internal params away from user interactions
|
||||||
|
_VALID_ARGS = frozenset(('path', 'state', 'table', 'noflush', 'counters', 'modprobe', 'ip_version', 'wait'))
|
||||||
|
DEFAULT_SUDOABLE = True
|
||||||
|
|
||||||
|
MSG_ERROR__ASYNC_AND_POLL_NOT_ZERO = (
|
||||||
|
"This module doesn't support async>0 and poll>0 when its 'state' param "
|
||||||
|
"is set to 'restored'. To enable its rollback feature (that needs the "
|
||||||
|
"module to run asynchronously on the remote), please set task attribute "
|
||||||
|
"'poll' (=%s) to 0, and 'async' (=%s) to a value >2 and not greater than "
|
||||||
|
"'ansible_timeout' (=%s) (recommended).")
|
||||||
|
MSG_WARNING__NO_ASYNC_IS_NO_ROLLBACK = (
|
||||||
|
"Attempts to restore iptables state without rollback in case of mistake "
|
||||||
|
"may lead the ansible controller to loose access to the hosts and never "
|
||||||
|
"regain it before fixing firewall rules through a serial console, or any "
|
||||||
|
"other way except SSH. Please set task attribute 'poll' (=%s) to 0, and "
|
||||||
|
"'async' (=%s) to a value >2 and not greater than 'ansible_timeout' (=%s) "
|
||||||
|
"(recommended).")
|
||||||
|
MSG_WARNING__ASYNC_GREATER_THAN_TIMEOUT = (
|
||||||
|
"You attempt to restore iptables state with rollback in case of mistake, "
|
||||||
|
"but with settings that will lead this rollback to happen AFTER that the "
|
||||||
|
"controller will reach its own timeout. Please set task attribute 'poll' "
|
||||||
|
"(=%s) to 0, and 'async' (=%s) to a value >2 and not greater than "
|
||||||
|
"'ansible_timeout' (=%s) (recommended).")
|
||||||
|
|
||||||
|
def _async_result(self, module_args, task_vars, timeout):
|
||||||
|
'''
|
||||||
|
Retrieve results of the asynchonous task, and display them in place of
|
||||||
|
the async wrapper results (those with the ansible_job_id key).
|
||||||
|
'''
|
||||||
|
# At least one iteration is required, even if timeout is 0.
|
||||||
|
for i in range(max(1, timeout)):
|
||||||
|
async_result = self._execute_module(
|
||||||
|
module_name='async_status',
|
||||||
|
module_args=module_args,
|
||||||
|
task_vars=task_vars,
|
||||||
|
wrap_async=False)
|
||||||
|
if async_result['finished'] == 1:
|
||||||
|
break
|
||||||
|
time.sleep(min(1, timeout))
|
||||||
|
|
||||||
|
return async_result
|
||||||
|
|
||||||
|
def run(self, tmp=None, task_vars=None):
|
||||||
|
|
||||||
|
self._supports_check_mode = True
|
||||||
|
self._supports_async = True
|
||||||
|
|
||||||
|
result = super(ActionModule, self).run(tmp, task_vars)
|
||||||
|
del tmp # tmp no longer has any effect
|
||||||
|
|
||||||
|
if not result.get('skipped'):
|
||||||
|
|
||||||
|
# FUTURE: better to let _execute_module calculate this internally?
|
||||||
|
wrap_async = self._task.async_val and not self._connection.has_native_async
|
||||||
|
|
||||||
|
# Set short names for values we'll have to compare or reuse
|
||||||
|
task_poll = self._task.poll
|
||||||
|
task_async = self._task.async_val
|
||||||
|
check_mode = self._play_context.check_mode
|
||||||
|
max_timeout = self._connection._play_context.timeout
|
||||||
|
module_name = self._task.action
|
||||||
|
module_args = self._task.args
|
||||||
|
|
||||||
|
if module_args.get('state', None) == 'restored':
|
||||||
|
if not wrap_async:
|
||||||
|
if not check_mode:
|
||||||
|
display.warning(self.MSG_WARNING__NO_ASYNC_IS_NO_ROLLBACK % (
|
||||||
|
task_poll,
|
||||||
|
task_async,
|
||||||
|
max_timeout))
|
||||||
|
elif task_poll:
|
||||||
|
raise AnsibleActionFail(self.MSG_ERROR__ASYNC_AND_POLL_NOT_ZERO % (
|
||||||
|
task_poll,
|
||||||
|
task_async,
|
||||||
|
max_timeout))
|
||||||
|
else:
|
||||||
|
if task_async > max_timeout and not check_mode:
|
||||||
|
display.warning(self.MSG_WARNING__ASYNC_GREATER_THAN_TIMEOUT % (
|
||||||
|
task_poll,
|
||||||
|
task_async,
|
||||||
|
max_timeout))
|
||||||
|
|
||||||
|
# BEGIN snippet from async_status action plugin
|
||||||
|
env_async_dir = [e for e in self._task.environment if
|
||||||
|
"ANSIBLE_ASYNC_DIR" in e]
|
||||||
|
if len(env_async_dir) > 0:
|
||||||
|
# for backwards compatibility we need to get the dir from
|
||||||
|
# ANSIBLE_ASYNC_DIR that is defined in the environment. This is
|
||||||
|
# deprecated and will be removed in favour of shell options
|
||||||
|
async_dir = env_async_dir[0]['ANSIBLE_ASYNC_DIR']
|
||||||
|
|
||||||
|
msg = "Setting the async dir from the environment keyword " \
|
||||||
|
"ANSIBLE_ASYNC_DIR is deprecated. Set the async_dir " \
|
||||||
|
"shell option instead"
|
||||||
|
display.deprecated(msg, version='2.0.0',
|
||||||
|
collection_name='community.general') # was Ansible 2.12
|
||||||
|
else:
|
||||||
|
# inject the async directory based on the shell option into the
|
||||||
|
# module args
|
||||||
|
async_dir = self.get_shell_option('async_dir', default="~/.ansible_async")
|
||||||
|
# END snippet from async_status action plugin
|
||||||
|
|
||||||
|
# Bind the loop max duration to consistent values on both
|
||||||
|
# remote and local sides (if not the same, make the loop
|
||||||
|
# longer on the controller); and set a backup file path.
|
||||||
|
module_args['_timeout'] = task_async
|
||||||
|
module_args['_back'] = '%s/iptables.state' % async_dir
|
||||||
|
async_status_args = dict(_async_dir=async_dir)
|
||||||
|
confirm_cmd = 'rm -f %s' % module_args['_back']
|
||||||
|
remaining_time = max(task_async, max_timeout)
|
||||||
|
|
||||||
|
# do work!
|
||||||
|
result = merge_hash(result, self._execute_module(module_args=module_args, task_vars=task_vars, wrap_async=wrap_async))
|
||||||
|
|
||||||
|
# Then the 3-steps "go ahead or rollback":
|
||||||
|
# - reset connection to ensure a persistent one will not be reused
|
||||||
|
# - confirm the restored state by removing the backup on the remote
|
||||||
|
# - retrieve the results of the asynchronous task to return them
|
||||||
|
if '_back' in module_args:
|
||||||
|
async_status_args['jid'] = result.get('ansible_job_id', None)
|
||||||
|
if async_status_args['jid'] is None:
|
||||||
|
raise AnsibleActionFail("Unable to get 'ansible_job_id'.")
|
||||||
|
|
||||||
|
# Catch early errors due to missing mandatory option, bad
|
||||||
|
# option type/value, missing required system command, etc.
|
||||||
|
result = merge_hash(result, self._async_result(async_status_args, task_vars, 0))
|
||||||
|
|
||||||
|
if not result['finished']:
|
||||||
|
try:
|
||||||
|
self._connection.reset()
|
||||||
|
display.v("%s: reset connection" % (module_name))
|
||||||
|
except AttributeError:
|
||||||
|
display.warning("Connection plugin does not allow to reset the connection.")
|
||||||
|
|
||||||
|
for x in range(max_timeout):
|
||||||
|
time.sleep(1)
|
||||||
|
remaining_time -= 1
|
||||||
|
# - AnsibleConnectionFailure covers rejected requests (i.e.
|
||||||
|
# by rules with '--jump REJECT')
|
||||||
|
# - ansible_timeout is able to cover dropped requests (due
|
||||||
|
# to a rule or policy DROP) if not lower than async_val.
|
||||||
|
try:
|
||||||
|
garbage = self._low_level_execute_command(confirm_cmd, sudoable=self.DEFAULT_SUDOABLE)
|
||||||
|
break
|
||||||
|
except AnsibleConnectionFailure:
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = merge_hash(result, self._async_result(async_status_args, task_vars, remaining_time))
|
||||||
|
|
||||||
|
# Cleanup async related stuff and internal params
|
||||||
|
for key in ('ansible_job_id', 'results_file', 'started', 'finished'):
|
||||||
|
if result.get(key):
|
||||||
|
del result[key]
|
||||||
|
|
||||||
|
if result.get('invocation', {}).get('module_args'):
|
||||||
|
if '_timeout' in result['invocation']['module_args']:
|
||||||
|
del result['invocation']['module_args']['_back']
|
||||||
|
del result['invocation']['module_args']['_timeout']
|
||||||
|
|
||||||
|
async_status_args['mode'] = 'cleanup'
|
||||||
|
garbage = self._execute_module(
|
||||||
|
module_name='async_status',
|
||||||
|
module_args=async_status_args,
|
||||||
|
task_vars=task_vars,
|
||||||
|
wrap_async=False)
|
||||||
|
|
||||||
|
if not wrap_async:
|
||||||
|
# remove a temporary path we created
|
||||||
|
self._remove_tmp_path(self._connection._shell.tmpdir)
|
||||||
|
|
||||||
|
return result
|
1
plugins/modules/iptables_state.py
Symbolic link
1
plugins/modules/iptables_state.py
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
system/iptables_state.py
|
637
plugins/modules/system/iptables_state.py
Normal file
637
plugins/modules/system/iptables_state.py
Normal file
|
@ -0,0 +1,637 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2020, quidame <quidame@poivron.org>
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: iptables_state
|
||||||
|
short_description: Save iptables state into a file or restore it from a file
|
||||||
|
version_added: '1.1.0'
|
||||||
|
author: quidame (@quidame)
|
||||||
|
description:
|
||||||
|
- C(iptables) is used to set up, maintain, and inspect the tables of IP
|
||||||
|
packet filter rules in the Linux kernel.
|
||||||
|
- This module handles the saving and/or loading of rules. This is the same
|
||||||
|
as the behaviour of the C(iptables-save) and C(iptables-restore) (or
|
||||||
|
C(ip6tables-save) and C(ip6tables-restore) for IPv6) commands which this
|
||||||
|
module uses internally.
|
||||||
|
- Modifying the state of the firewall remotely may lead to loose access to
|
||||||
|
the host in case of mistake in new ruleset. This module embeds a rollback
|
||||||
|
feature to avoid this, by telling the host to restore previous rules if a
|
||||||
|
cookie is still there after a given delay, and all this time telling the
|
||||||
|
controller to try to remove this cookie on the host through a new
|
||||||
|
connection.
|
||||||
|
notes:
|
||||||
|
- The rollback feature is not a module option and depends on task's
|
||||||
|
attributes. To enable it, the module must be played asynchronously, i.e.
|
||||||
|
by setting task attributes I(poll) to C(0), and I(async) to a value less
|
||||||
|
or equal to C(ANSIBLE_TIMEOUT). If I(async) is greater, the rollback will
|
||||||
|
still happen if it shall happen, but you will experience a connection
|
||||||
|
timeout instead of more relevant info returned by the module after its
|
||||||
|
failure.
|
||||||
|
- This module supports I(check_mode).
|
||||||
|
options:
|
||||||
|
counters:
|
||||||
|
description:
|
||||||
|
- Save or restore the values of all packet and byte counters.
|
||||||
|
- When C(true), the module is not idempotent.
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
ip_version:
|
||||||
|
description:
|
||||||
|
- Which version of the IP protocol this module should apply to.
|
||||||
|
type: str
|
||||||
|
choices: [ ipv4, ipv6 ]
|
||||||
|
default: ipv4
|
||||||
|
modprobe:
|
||||||
|
description:
|
||||||
|
- Specify the path to the C(modprobe) program internally used by iptables
|
||||||
|
related commands to load kernel modules.
|
||||||
|
- By default, C(/proc/sys/kernel/modprobe) is inspected to determine the
|
||||||
|
executable's path.
|
||||||
|
type: path
|
||||||
|
noflush:
|
||||||
|
description:
|
||||||
|
- For I(state=restored), ignored otherwise.
|
||||||
|
- If C(false), restoring iptables rules from a file flushes (deletes)
|
||||||
|
all previous contents of the respective table(s). If C(true), the
|
||||||
|
previous rules are left untouched (but policies are updated anyway,
|
||||||
|
for all built-in chains).
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
path:
|
||||||
|
description:
|
||||||
|
- The file the iptables state should be saved to.
|
||||||
|
- The file the iptables state should be restored from.
|
||||||
|
type: path
|
||||||
|
required: yes
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the firewall state should be saved (into a file) or restored
|
||||||
|
(from a file).
|
||||||
|
type: str
|
||||||
|
choices: [ saved, restored ]
|
||||||
|
required: yes
|
||||||
|
table:
|
||||||
|
description:
|
||||||
|
- When I(state=restored), restore only the named table even if the input
|
||||||
|
file contains other tables. Fail if the named table is not declared in
|
||||||
|
the file.
|
||||||
|
- When I(state=saved), restrict output to the specified table. If not
|
||||||
|
specified, output includes all active tables.
|
||||||
|
type: str
|
||||||
|
choices: [ filter, nat, mangle, raw, security ]
|
||||||
|
wait:
|
||||||
|
description:
|
||||||
|
- Wait N seconds for the xtables lock to prevent instant failure in case
|
||||||
|
multiple instances of the program are running concurrently.
|
||||||
|
type: int
|
||||||
|
requirements: [iptables, ip6tables]
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
# This will apply to all loaded/active IPv4 tables.
|
||||||
|
- name: Save current state of the firewall in system file
|
||||||
|
community.general.iptables_state:
|
||||||
|
state: saved
|
||||||
|
path: /etc/sysconfig/iptables
|
||||||
|
|
||||||
|
# This will apply only to IPv6 filter table.
|
||||||
|
- name: save current state of the firewall in system file
|
||||||
|
community.general.iptables_state:
|
||||||
|
ip_version: ipv6
|
||||||
|
table: filter
|
||||||
|
state: saved
|
||||||
|
path: /etc/iptables/rules.v6
|
||||||
|
|
||||||
|
# This will load a state from a file, with a rollback in case of access loss
|
||||||
|
- name: restore firewall state from a file
|
||||||
|
community.general.iptables_state:
|
||||||
|
state: restored
|
||||||
|
path: /run/iptables.apply
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
# This will load new rules by appending them to the current ones
|
||||||
|
- name: restore firewall state from a file
|
||||||
|
community.general.iptables_state:
|
||||||
|
state: restored
|
||||||
|
path: /run/iptables.apply
|
||||||
|
noflush: true
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
# This will only retrieve information
|
||||||
|
- name: get current state of the firewall
|
||||||
|
community.general.iptables_state:
|
||||||
|
state: saved
|
||||||
|
path: /tmp/iptables
|
||||||
|
check_mode: yes
|
||||||
|
changed_when: false
|
||||||
|
register: iptables_state
|
||||||
|
|
||||||
|
- name: show current state of the firewall
|
||||||
|
ansible.builtin.debug:
|
||||||
|
var: iptables_state.initial_state
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
applied:
|
||||||
|
description: Whether or not the wanted state has been successfully restored.
|
||||||
|
type: bool
|
||||||
|
returned: always
|
||||||
|
sample: true
|
||||||
|
initial_state:
|
||||||
|
description: The current state of the firewall when module starts.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
returned: always
|
||||||
|
sample: [
|
||||||
|
"# Generated by xtables-save v1.8.2",
|
||||||
|
"*filter",
|
||||||
|
":INPUT ACCEPT [0:0]",
|
||||||
|
":FORWARD ACCEPT [0:0]",
|
||||||
|
":OUTPUT ACCEPT [0:0]",
|
||||||
|
"COMMIT",
|
||||||
|
"# Completed"
|
||||||
|
]
|
||||||
|
restored:
|
||||||
|
description: The state the module restored, whenever it is finally applied or not.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
returned: always
|
||||||
|
sample: [
|
||||||
|
"# Generated by xtables-save v1.8.2",
|
||||||
|
"*filter",
|
||||||
|
":INPUT DROP [0:0]",
|
||||||
|
":FORWARD DROP [0:0]",
|
||||||
|
":OUTPUT ACCEPT [0:0]",
|
||||||
|
"-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT",
|
||||||
|
"-A INPUT -m conntrack --ctstate INVALID -j DROP",
|
||||||
|
"-A INPUT -i lo -j ACCEPT",
|
||||||
|
"-A INPUT -p icmp -j ACCEPT",
|
||||||
|
"-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT",
|
||||||
|
"COMMIT",
|
||||||
|
"# Completed"
|
||||||
|
]
|
||||||
|
saved:
|
||||||
|
description: The iptables state the module saved.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
returned: always
|
||||||
|
sample: [
|
||||||
|
"# Generated by xtables-save v1.8.2",
|
||||||
|
"*filter",
|
||||||
|
":INPUT ACCEPT [0:0]",
|
||||||
|
":FORWARD DROP [0:0]",
|
||||||
|
":OUTPUT ACCEPT [0:0]",
|
||||||
|
"COMMIT",
|
||||||
|
"# Completed"
|
||||||
|
]
|
||||||
|
tables:
|
||||||
|
description: The iptables we have interest for when module starts.
|
||||||
|
type: dict
|
||||||
|
contains:
|
||||||
|
table:
|
||||||
|
description: Policies and rules for all chains of the named table.
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
sample: |-
|
||||||
|
{
|
||||||
|
"filter": [
|
||||||
|
":INPUT ACCEPT",
|
||||||
|
":FORWARD ACCEPT",
|
||||||
|
":OUTPUT ACCEPT",
|
||||||
|
"-A INPUT -i lo -j ACCEPT",
|
||||||
|
"-A INPUT -p icmp -j ACCEPT",
|
||||||
|
"-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT",
|
||||||
|
"-A INPUT -j REJECT --reject-with icmp-host-prohibited"
|
||||||
|
],
|
||||||
|
"nat": [
|
||||||
|
":PREROUTING ACCEPT",
|
||||||
|
":INPUT ACCEPT",
|
||||||
|
":OUTPUT ACCEPT",
|
||||||
|
":POSTROUTING ACCEPT"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
returned: always
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import tempfile
|
||||||
|
import filecmp
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
from ansible.module_utils._text import to_bytes, to_native, to_text
|
||||||
|
|
||||||
|
|
||||||
|
IPTABLES = dict(
|
||||||
|
ipv4='iptables',
|
||||||
|
ipv6='ip6tables',
|
||||||
|
)
|
||||||
|
|
||||||
|
SAVE = dict(
|
||||||
|
ipv4='iptables-save',
|
||||||
|
ipv6='ip6tables-save',
|
||||||
|
)
|
||||||
|
|
||||||
|
RESTORE = dict(
|
||||||
|
ipv4='iptables-restore',
|
||||||
|
ipv6='ip6tables-restore',
|
||||||
|
)
|
||||||
|
|
||||||
|
TABLES = ['filter', 'mangle', 'nat', 'raw', 'security']
|
||||||
|
|
||||||
|
|
||||||
|
def read_state(b_path):
|
||||||
|
'''
|
||||||
|
Read a file and store its content in a variable as a list.
|
||||||
|
'''
|
||||||
|
with open(b_path, 'r') as f:
|
||||||
|
text = f.read()
|
||||||
|
lines = text.splitlines()
|
||||||
|
while '' in lines:
|
||||||
|
lines.remove('')
|
||||||
|
return (lines)
|
||||||
|
|
||||||
|
|
||||||
|
def write_state(b_path, lines, changed):
|
||||||
|
'''
|
||||||
|
Write given contents to the given path, and return changed status.
|
||||||
|
'''
|
||||||
|
# Populate a temporary file
|
||||||
|
tmpfd, tmpfile = tempfile.mkstemp()
|
||||||
|
with os.fdopen(tmpfd, 'w') as f:
|
||||||
|
for line in lines:
|
||||||
|
f.write('%s\n' % line)
|
||||||
|
|
||||||
|
# Prepare to copy temporary file to the final destination
|
||||||
|
if not os.path.exists(b_path):
|
||||||
|
b_destdir = os.path.dirname(b_path)
|
||||||
|
destdir = to_native(b_destdir, errors='surrogate_or_strict')
|
||||||
|
if b_destdir and not os.path.exists(b_destdir) and not module.check_mode:
|
||||||
|
try:
|
||||||
|
os.makedirs(b_destdir)
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(
|
||||||
|
msg='Error creating %s. Error code: %s. Error description: %s' % (destdir, e[0], e[1]),
|
||||||
|
initial_state=lines)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
elif not filecmp.cmp(tmpfile, b_path):
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Do it
|
||||||
|
if changed and not module.check_mode:
|
||||||
|
try:
|
||||||
|
shutil.copyfile(tmpfile, b_path)
|
||||||
|
except Exception as e:
|
||||||
|
path = to_native(b_path, errors='surrogate_or_strict')
|
||||||
|
module.fail_json(
|
||||||
|
msg='Error saving state into %s. Error code: %s. Error description: %s' % (path, e[0], e[1]),
|
||||||
|
initial_state=lines)
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_from_null_state(initializer, initcommand, table):
|
||||||
|
'''
|
||||||
|
This ensures iptables-state output is suitable for iptables-restore to roll
|
||||||
|
back to it, i.e. iptables-save output is not empty. This also works for the
|
||||||
|
iptables-nft-save alternative.
|
||||||
|
'''
|
||||||
|
if table is None:
|
||||||
|
table = 'filter'
|
||||||
|
|
||||||
|
tmpfd, tmpfile = tempfile.mkstemp()
|
||||||
|
with os.fdopen(tmpfd, 'w') as f:
|
||||||
|
f.write('*%s\nCOMMIT\n' % table)
|
||||||
|
|
||||||
|
initializer.append(tmpfile)
|
||||||
|
(rc, out, err) = module.run_command(initializer, check_rc=True)
|
||||||
|
(rc, out, err) = module.run_command(initcommand, check_rc=True)
|
||||||
|
return (rc, out, err)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_and_format_state(string):
|
||||||
|
'''
|
||||||
|
Remove timestamps to ensure idempotence between runs. Also remove counters
|
||||||
|
by default. And return the result as a list.
|
||||||
|
'''
|
||||||
|
string = re.sub('((^|\n)# (Generated|Completed)[^\n]*) on [^\n]*', '\\1', string)
|
||||||
|
if not module.params['counters']:
|
||||||
|
string = re.sub('[[][0-9]+:[0-9]+[]]', '[0:0]', string)
|
||||||
|
lines = string.splitlines()
|
||||||
|
while '' in lines:
|
||||||
|
lines.remove('')
|
||||||
|
return (lines)
|
||||||
|
|
||||||
|
|
||||||
|
def per_table_state(command, state):
|
||||||
|
'''
|
||||||
|
Convert raw iptables-save output into usable datastructure, for reliable
|
||||||
|
comparisons between initial and final states.
|
||||||
|
'''
|
||||||
|
tables = dict()
|
||||||
|
for t in TABLES:
|
||||||
|
COMMAND = list(command)
|
||||||
|
if '*%s' % t in state.splitlines():
|
||||||
|
COMMAND.extend(['--table', t])
|
||||||
|
(rc, out, err) = module.run_command(COMMAND, check_rc=True)
|
||||||
|
out = re.sub('(^|\n)(# Generated|# Completed|[*]%s|COMMIT)[^\n]*' % t, '', out)
|
||||||
|
out = re.sub(' *[[][0-9]+:[0-9]+[]] *', '', out)
|
||||||
|
table = out.splitlines()
|
||||||
|
while '' in table:
|
||||||
|
table.remove('')
|
||||||
|
tables[t] = table
|
||||||
|
return (tables)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
global module
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
path=dict(type='path', required=True),
|
||||||
|
state=dict(type='str', choices=['saved', 'restored'], required=True),
|
||||||
|
table=dict(type='str', choices=['filter', 'nat', 'mangle', 'raw', 'security']),
|
||||||
|
noflush=dict(type='bool', default=False),
|
||||||
|
counters=dict(type='bool', default=False),
|
||||||
|
modprobe=dict(type='path'),
|
||||||
|
ip_version=dict(type='str', choices=['ipv4', 'ipv6'], default='ipv4'),
|
||||||
|
wait=dict(type='int'),
|
||||||
|
_timeout=dict(type='int'),
|
||||||
|
_back=dict(type='path'),
|
||||||
|
),
|
||||||
|
required_together=[
|
||||||
|
['_timeout', '_back'],
|
||||||
|
],
|
||||||
|
supports_check_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# We'll parse iptables-restore stderr
|
||||||
|
module.run_command_environ_update = dict(LANG='C', LC_MESSAGES='C')
|
||||||
|
|
||||||
|
path = module.params['path']
|
||||||
|
state = module.params['state']
|
||||||
|
table = module.params['table']
|
||||||
|
noflush = module.params['noflush']
|
||||||
|
counters = module.params['counters']
|
||||||
|
modprobe = module.params['modprobe']
|
||||||
|
ip_version = module.params['ip_version']
|
||||||
|
wait = module.params['wait']
|
||||||
|
_timeout = module.params['_timeout']
|
||||||
|
_back = module.params['_back']
|
||||||
|
|
||||||
|
bin_iptables = module.get_bin_path(IPTABLES[ip_version], True)
|
||||||
|
bin_iptables_save = module.get_bin_path(SAVE[ip_version], True)
|
||||||
|
bin_iptables_restore = module.get_bin_path(RESTORE[ip_version], True)
|
||||||
|
|
||||||
|
os.umask(0o077)
|
||||||
|
changed = False
|
||||||
|
COMMANDARGS = []
|
||||||
|
INITCOMMAND = [bin_iptables_save]
|
||||||
|
INITIALIZER = [bin_iptables_restore]
|
||||||
|
TESTCOMMAND = [bin_iptables_restore, '--test']
|
||||||
|
|
||||||
|
if counters:
|
||||||
|
COMMANDARGS.append('--counters')
|
||||||
|
|
||||||
|
if table is not None:
|
||||||
|
COMMANDARGS.extend(['--table', table])
|
||||||
|
|
||||||
|
if wait is not None:
|
||||||
|
TESTCOMMAND.extend(['--wait', '%s' % wait])
|
||||||
|
|
||||||
|
if modprobe is not None:
|
||||||
|
b_modprobe = to_bytes(modprobe, errors='surrogate_or_strict')
|
||||||
|
if not os.path.exists(b_modprobe):
|
||||||
|
module.fail_json(msg="modprobe %s not found" % modprobe)
|
||||||
|
if not os.path.isfile(b_modprobe):
|
||||||
|
module.fail_json(msg="modprobe %s not a file" % modprobe)
|
||||||
|
if not os.access(b_modprobe, os.R_OK):
|
||||||
|
module.fail_json(msg="modprobe %s not readable" % modprobe)
|
||||||
|
if not os.access(b_modprobe, os.X_OK):
|
||||||
|
module.fail_json(msg="modprobe %s not executable" % modprobe)
|
||||||
|
COMMANDARGS.extend(['--modprobe', modprobe])
|
||||||
|
INITIALIZER.extend(['--modprobe', modprobe])
|
||||||
|
INITCOMMAND.extend(['--modprobe', modprobe])
|
||||||
|
TESTCOMMAND.extend(['--modprobe', modprobe])
|
||||||
|
|
||||||
|
SAVECOMMAND = list(COMMANDARGS)
|
||||||
|
SAVECOMMAND.insert(0, bin_iptables_save)
|
||||||
|
|
||||||
|
b_path = to_bytes(path, errors='surrogate_or_strict')
|
||||||
|
|
||||||
|
if state == 'restored':
|
||||||
|
if not os.path.exists(b_path):
|
||||||
|
module.fail_json(msg="Source %s not found" % path)
|
||||||
|
if not os.path.isfile(b_path):
|
||||||
|
module.fail_json(msg="Source %s not a file" % path)
|
||||||
|
if not os.access(b_path, os.R_OK):
|
||||||
|
module.fail_json(msg="Source %s not readable" % path)
|
||||||
|
state_to_restore = read_state(b_path)
|
||||||
|
else:
|
||||||
|
cmd = ' '.join(SAVECOMMAND)
|
||||||
|
|
||||||
|
(rc, stdout, stderr) = module.run_command(INITCOMMAND, check_rc=True)
|
||||||
|
|
||||||
|
# The issue comes when wanting to restore state from empty iptable-save's
|
||||||
|
# output... what happens when, say:
|
||||||
|
# - no table is specified, and iptables-save's output is only nat table;
|
||||||
|
# - we give filter's ruleset to iptables-restore, that locks ourselve out
|
||||||
|
# of the host;
|
||||||
|
# then trying to roll iptables state back to the previous (working) setup
|
||||||
|
# doesn't override current filter table because no filter table is stored
|
||||||
|
# in the backup ! So we have to ensure tables to be restored have a backup
|
||||||
|
# in case of rollback.
|
||||||
|
if table is None:
|
||||||
|
if state == 'restored':
|
||||||
|
for t in TABLES:
|
||||||
|
if '*%s' % t in state_to_restore:
|
||||||
|
if len(stdout) == 0 or '*%s' % t not in stdout.splitlines():
|
||||||
|
(rc, stdout, stderr) = initialize_from_null_state(INITIALIZER, INITCOMMAND, t)
|
||||||
|
elif len(stdout) == 0:
|
||||||
|
(rc, stdout, stderr) = initialize_from_null_state(INITIALIZER, INITCOMMAND, 'filter')
|
||||||
|
|
||||||
|
elif state == 'restored' and '*%s' % table not in state_to_restore:
|
||||||
|
module.fail_json(msg="Table %s to restore not defined in %s" % (table, path))
|
||||||
|
|
||||||
|
elif len(stdout) == 0 or '*%s' % table not in stdout.splitlines():
|
||||||
|
(rc, stdout, stderr) = initialize_from_null_state(INITIALIZER, INITCOMMAND, table)
|
||||||
|
|
||||||
|
initial_state = filter_and_format_state(stdout)
|
||||||
|
if initial_state is None:
|
||||||
|
module.fail_json(msg="Unable to initialize firewall from NULL state.")
|
||||||
|
|
||||||
|
# Depending on the value of 'table', initref_state may differ from
|
||||||
|
# initial_state.
|
||||||
|
(rc, stdout, stderr) = module.run_command(SAVECOMMAND, check_rc=True)
|
||||||
|
tables_before = per_table_state(SAVECOMMAND, stdout)
|
||||||
|
initref_state = filter_and_format_state(stdout)
|
||||||
|
|
||||||
|
if state == 'saved':
|
||||||
|
changed = write_state(b_path, initref_state, changed)
|
||||||
|
module.exit_json(
|
||||||
|
changed=changed,
|
||||||
|
cmd=cmd,
|
||||||
|
tables=tables_before,
|
||||||
|
initial_state=initial_state,
|
||||||
|
saved=initref_state)
|
||||||
|
|
||||||
|
#
|
||||||
|
# All remaining code is for state=restored
|
||||||
|
#
|
||||||
|
|
||||||
|
MAINCOMMAND = list(COMMANDARGS)
|
||||||
|
MAINCOMMAND.insert(0, bin_iptables_restore)
|
||||||
|
|
||||||
|
if wait is not None:
|
||||||
|
MAINCOMMAND.extend(['--wait', '%s' % wait])
|
||||||
|
|
||||||
|
if _back is not None:
|
||||||
|
b_back = to_bytes(_back, errors='surrogate_or_strict')
|
||||||
|
garbage = write_state(b_back, initref_state, changed)
|
||||||
|
BACKCOMMAND = list(MAINCOMMAND)
|
||||||
|
BACKCOMMAND.append(_back)
|
||||||
|
|
||||||
|
if noflush:
|
||||||
|
MAINCOMMAND.append('--noflush')
|
||||||
|
|
||||||
|
MAINCOMMAND.append(path)
|
||||||
|
cmd = ' '.join(MAINCOMMAND)
|
||||||
|
|
||||||
|
TESTCOMMAND = list(MAINCOMMAND)
|
||||||
|
TESTCOMMAND.insert(1, '--test')
|
||||||
|
error_msg = "Source %s is not suitable for input to %s" % (path, os.path.basename(bin_iptables_restore))
|
||||||
|
|
||||||
|
# Due to a bug in iptables-nft-restore --test, we have to validate tables
|
||||||
|
# one by one (https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=960003).
|
||||||
|
for t in tables_before:
|
||||||
|
testcommand = list(TESTCOMMAND)
|
||||||
|
testcommand.extend(['--table', t])
|
||||||
|
(rc, stdout, stderr) = module.run_command(testcommand)
|
||||||
|
|
||||||
|
if 'Another app is currently holding the xtables lock' in stderr:
|
||||||
|
error_msg = stderr
|
||||||
|
|
||||||
|
if rc != 0:
|
||||||
|
cmd = ' '.join(testcommand)
|
||||||
|
module.fail_json(
|
||||||
|
msg=error_msg,
|
||||||
|
cmd=cmd,
|
||||||
|
rc=rc,
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
|
tables=tables_before,
|
||||||
|
initial_state=initial_state,
|
||||||
|
restored=state_to_restore,
|
||||||
|
applied=False)
|
||||||
|
|
||||||
|
if module.check_mode:
|
||||||
|
tmpfd, tmpfile = tempfile.mkstemp()
|
||||||
|
with os.fdopen(tmpfd, 'w') as f:
|
||||||
|
for line in initial_state:
|
||||||
|
f.write('%s\n' % line)
|
||||||
|
|
||||||
|
if filecmp.cmp(tmpfile, b_path):
|
||||||
|
restored_state = initial_state
|
||||||
|
else:
|
||||||
|
restored_state = state_to_restore
|
||||||
|
|
||||||
|
else:
|
||||||
|
(rc, stdout, stderr) = module.run_command(MAINCOMMAND)
|
||||||
|
if 'Another app is currently holding the xtables lock' in stderr:
|
||||||
|
module.fail_json(
|
||||||
|
msg=stderr,
|
||||||
|
cmd=cmd,
|
||||||
|
rc=rc,
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
|
tables=tables_before,
|
||||||
|
initial_state=initial_state,
|
||||||
|
restored=state_to_restore,
|
||||||
|
applied=False)
|
||||||
|
|
||||||
|
(rc, stdout, stderr) = module.run_command(SAVECOMMAND, check_rc=True)
|
||||||
|
restored_state = filter_and_format_state(stdout)
|
||||||
|
|
||||||
|
if restored_state != initref_state and restored_state != initial_state:
|
||||||
|
if module.check_mode:
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
tables_after = per_table_state(SAVECOMMAND, stdout)
|
||||||
|
if tables_after != tables_before:
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if _back is None or module.check_mode:
|
||||||
|
module.exit_json(
|
||||||
|
changed=changed,
|
||||||
|
cmd=cmd,
|
||||||
|
tables=tables_before,
|
||||||
|
initial_state=initial_state,
|
||||||
|
restored=restored_state,
|
||||||
|
applied=True)
|
||||||
|
|
||||||
|
# The rollback implementation currently needs:
|
||||||
|
# Here:
|
||||||
|
# * test existence of the backup file, exit with success if it doesn't exist
|
||||||
|
# * otherwise, restore iptables from this file and return failure
|
||||||
|
# Action plugin:
|
||||||
|
# * try to remove the backup file
|
||||||
|
# * wait async task is finished and retrieve its final status
|
||||||
|
# * modify it and return the result
|
||||||
|
# Task:
|
||||||
|
# * task attribute 'async' set to the same value (or lower) than ansible
|
||||||
|
# timeout
|
||||||
|
# * task attribute 'poll' equals 0
|
||||||
|
#
|
||||||
|
for x in range(_timeout):
|
||||||
|
if os.path.exists(b_back):
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
module.exit_json(
|
||||||
|
changed=changed,
|
||||||
|
cmd=cmd,
|
||||||
|
tables=tables_before,
|
||||||
|
initial_state=initial_state,
|
||||||
|
restored=restored_state,
|
||||||
|
applied=True)
|
||||||
|
|
||||||
|
# Here we are: for whatever reason, but probably due to the current ruleset,
|
||||||
|
# the action plugin (i.e. on the controller) was unable to remove the backup
|
||||||
|
# cookie, so we restore initial state from it.
|
||||||
|
(rc, stdout, stderr) = module.run_command(BACKCOMMAND, check_rc=True)
|
||||||
|
os.remove(b_back)
|
||||||
|
|
||||||
|
(rc, stdout, stderr) = module.run_command(SAVECOMMAND, check_rc=True)
|
||||||
|
tables_rollback = per_table_state(SAVECOMMAND, stdout)
|
||||||
|
|
||||||
|
msg = (
|
||||||
|
"Failed to confirm state restored from %s after %ss. "
|
||||||
|
"Firewall has been rolled back to its initial state." % (path, _timeout)
|
||||||
|
)
|
||||||
|
|
||||||
|
module.fail_json(
|
||||||
|
changed=(tables_before != tables_rollback),
|
||||||
|
msg=msg,
|
||||||
|
cmd=cmd,
|
||||||
|
tables=tables_before,
|
||||||
|
initial_state=initial_state,
|
||||||
|
restored=restored_state,
|
||||||
|
applied=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
6
tests/integration/targets/iptables_state/aliases
Normal file
6
tests/integration/targets/iptables_state/aliases
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
destructive
|
||||||
|
shippable/posix/group4
|
||||||
|
skip/docker # kernel modules not loadable
|
||||||
|
skip/freebsd # no iptables/netfilter (Linux specific)
|
||||||
|
skip/osx # no iptables/netfilter (Linux specific)
|
||||||
|
skip/aix # no iptables/netfilter (Linux specific)
|
2
tests/integration/targets/iptables_state/meta/main.yml
Normal file
2
tests/integration/targets/iptables_state/meta/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
dependencies:
|
||||||
|
- setup_pkg_mgr
|
29
tests/integration/targets/iptables_state/tasks/main.yml
Normal file
29
tests/integration/targets/iptables_state/tasks/main.yml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
- name: ensure iptables package is installed
|
||||||
|
package:
|
||||||
|
name:
|
||||||
|
- iptables
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
|
||||||
|
- name: include tasks
|
||||||
|
vars:
|
||||||
|
iptables_saved: "/tmp/test_iptables_state.saved"
|
||||||
|
iptables_tests: "/tmp/test_iptables_state.tests"
|
||||||
|
|
||||||
|
block:
|
||||||
|
- name: include tasks to perform basic tests (check_mode, async, idempotency)
|
||||||
|
include_tasks: tests/00-basic.yml
|
||||||
|
|
||||||
|
- name: include tasks to test tables handling
|
||||||
|
include_tasks: tests/01-tables.yml
|
||||||
|
when:
|
||||||
|
- xtables_lock is undefined
|
||||||
|
|
||||||
|
- name: include tasks to test rollbacks
|
||||||
|
include_tasks: tests/10-rollback.yml
|
||||||
|
when:
|
||||||
|
- xtables_lock is undefined
|
||||||
|
- ansible_connection in ['ssh', 'paramiko', 'smart']
|
||||||
|
|
||||||
|
become: yes
|
|
@ -0,0 +1,316 @@
|
||||||
|
---
|
||||||
|
- name: "ensure our next backup is not there (file)"
|
||||||
|
file:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: "ensure our next rule is not there (iptables)"
|
||||||
|
iptables:
|
||||||
|
chain: OUTPUT
|
||||||
|
jump: ACCEPT
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Basic checks about invalid param/value handling.
|
||||||
|
#
|
||||||
|
- name: "trigger error about invalid param"
|
||||||
|
iptables_state:
|
||||||
|
name: foobar
|
||||||
|
register: iptables_state
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
- iptables_state.msg is match("Invalid options")
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "trigger error about missing param 'state'"
|
||||||
|
iptables_state:
|
||||||
|
path: foobar
|
||||||
|
register: iptables_state
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
- iptables_state.msg is match("missing required arguments")
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "trigger error about missing param 'path'"
|
||||||
|
iptables_state:
|
||||||
|
state: saved
|
||||||
|
register: iptables_state
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
- iptables_state.msg is match("missing required arguments")
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "trigger error about invalid value for param 'state'"
|
||||||
|
iptables_state:
|
||||||
|
path: foobar
|
||||||
|
state: present
|
||||||
|
register: iptables_state
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
- iptables_state.msg is match("value of state must be one of")
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Play with the current state first. We will create a file to store it in, but
|
||||||
|
# no more. These tests are for:
|
||||||
|
# - idempotency
|
||||||
|
# - check_mode
|
||||||
|
#
|
||||||
|
- name: "save state (check_mode, must report a change)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: saved
|
||||||
|
register: iptables_state
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is changed
|
||||||
|
- iptables_state.initial_state == iptables_state.saved
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "save state (must report a change)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: saved
|
||||||
|
register: iptables_state
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is changed
|
||||||
|
- iptables_state.initial_state == iptables_state.saved
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "save state (idempotency, must NOT report a change)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: saved
|
||||||
|
register: iptables_state
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
- iptables_state.initial_state == iptables_state.saved
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "save state (check_mode, must NOT report a change)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: saved
|
||||||
|
register: iptables_state
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
- iptables_state.initial_state == iptables_state.saved
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# We begin with 'state=restored' by restoring the current state on itself.
|
||||||
|
# This at least ensures the file produced with state=saved is suitable for
|
||||||
|
# state=restored.
|
||||||
|
|
||||||
|
- name: "state=restored check_mode=true changed=false"
|
||||||
|
block:
|
||||||
|
- name: "restore state (check_mode, must NOT report a change, no warning)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
- iptables_state.initial_state == iptables_state.restored
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: "assert that results are not as expected for only one reason (xtables lock)"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
- iptables_state.stderr is search('xtables lock')
|
||||||
|
quiet: yes
|
||||||
|
register: xtables_lock
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "state=restored changed=false"
|
||||||
|
block:
|
||||||
|
- name: "restore state (must NOT report a change, warning about rollback & async)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
- iptables_state.initial_state == iptables_state.restored
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: "assert that results are not as expected for only one reason (xtables lock)"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
- iptables_state.stderr is search('xtables lock')
|
||||||
|
quiet: yes
|
||||||
|
register: xtables_lock
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "change iptables state (iptables)"
|
||||||
|
iptables:
|
||||||
|
chain: OUTPUT
|
||||||
|
jump: ACCEPT
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "state=restored changed=true"
|
||||||
|
block:
|
||||||
|
- name: "restore state (check_mode, must report a change)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is changed
|
||||||
|
- iptables_state.initial_state != iptables_state.restored
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: "assert that results are not as expected for only one reason (xtables lock)"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
- iptables_state.stderr is search('xtables lock')
|
||||||
|
quiet: yes
|
||||||
|
register: xtables_lock
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "state=restored changed=true"
|
||||||
|
block:
|
||||||
|
- name: "restore state (must report a change, async, no warning)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is changed
|
||||||
|
- iptables_state.initial_state != iptables_state.restored
|
||||||
|
- iptables_state.applied
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: "assert that results are not as expected for only one reason (xtables lock)"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
- iptables_state.stderr is search('xtables lock')
|
||||||
|
quiet: yes
|
||||||
|
register: xtables_lock
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "state=restored changed=false"
|
||||||
|
block:
|
||||||
|
- name: "restore state (must NOT report a change, async, no warning)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
- iptables_state.initial_state == iptables_state.restored
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: "assert that results are not as expected for only one reason (xtables lock)"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
- iptables_state.stderr is search('xtables lock')
|
||||||
|
quiet: yes
|
||||||
|
register: xtables_lock
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "state=restored changed=false"
|
||||||
|
block:
|
||||||
|
- name: "restore state (check_mode=yes, must NOT report a change, no warning)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
- iptables_state.initial_state == iptables_state.restored
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: "assert that results are not as expected for only one reason (xtables lock)"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
- iptables_state.stderr is search('xtables lock')
|
||||||
|
quiet: yes
|
||||||
|
register: xtables_lock
|
|
@ -0,0 +1,299 @@
|
||||||
|
---
|
||||||
|
- name: "ensure our next rule is not there (iptables)"
|
||||||
|
iptables:
|
||||||
|
table: nat
|
||||||
|
chain: INPUT
|
||||||
|
jump: ACCEPT
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: "get state (table filter)"
|
||||||
|
iptables_state:
|
||||||
|
table: filter
|
||||||
|
state: saved
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
register: iptables_state
|
||||||
|
changed_when: false
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'*filter' in iptables_state.initial_state"
|
||||||
|
- iptables_state.tables.filter is defined
|
||||||
|
- iptables_state.tables.nat is undefined
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "get state (table nat)"
|
||||||
|
iptables_state:
|
||||||
|
table: nat
|
||||||
|
state: saved
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
register: iptables_state
|
||||||
|
changed_when: false
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'*nat' in iptables_state.initial_state"
|
||||||
|
- "'*filter' in iptables_state.initial_state"
|
||||||
|
- iptables_state.tables.nat is defined
|
||||||
|
- iptables_state.tables.filter is undefined
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "save state (table filter)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: saved
|
||||||
|
table: filter
|
||||||
|
register: iptables_state
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'*filter' in iptables_state.initial_state"
|
||||||
|
- "'*filter' in iptables_state.saved"
|
||||||
|
- "'*nat' in iptables_state.initial_state"
|
||||||
|
- "'*nat' not in iptables_state.saved"
|
||||||
|
- iptables_state.tables.filter is defined
|
||||||
|
- iptables_state.tables.nat is undefined
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "save state (table nat)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: saved
|
||||||
|
table: nat
|
||||||
|
register: iptables_state
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is changed
|
||||||
|
- "'*nat' in iptables_state.initial_state"
|
||||||
|
- "'*nat' in iptables_state.saved"
|
||||||
|
- "'*filter' in iptables_state.initial_state"
|
||||||
|
- "'*filter' not in iptables_state.saved"
|
||||||
|
- iptables_state.tables.nat is defined
|
||||||
|
- iptables_state.tables.filter is undefined
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "save state (any table)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: saved
|
||||||
|
register: iptables_state
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is changed
|
||||||
|
- "'*filter' in iptables_state.initial_state"
|
||||||
|
- "'*filter' in iptables_state.saved"
|
||||||
|
- "'*nat' in iptables_state.initial_state"
|
||||||
|
- "'*nat' in iptables_state.saved"
|
||||||
|
- iptables_state.tables.filter is defined
|
||||||
|
- iptables_state.tables.nat is defined
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "restore state (table nat, must NOT report a change, no warning)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: restored
|
||||||
|
table: nat
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'*nat' in iptables_state.initial_state"
|
||||||
|
- "'*nat' in iptables_state.restored"
|
||||||
|
- "'*filter' in iptables_state.initial_state"
|
||||||
|
- "'*filter' not in iptables_state.restored"
|
||||||
|
- iptables_state.tables.nat is defined
|
||||||
|
- iptables_state.tables.filter is undefined
|
||||||
|
- iptables_state is not changed
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "change NAT table (iptables)"
|
||||||
|
iptables:
|
||||||
|
table: nat
|
||||||
|
chain: INPUT
|
||||||
|
jump: ACCEPT
|
||||||
|
state: present
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "restore state (table nat, must report a change, no warning)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: restored
|
||||||
|
table: nat
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'*nat' in iptables_state.initial_state"
|
||||||
|
- "'*nat' in iptables_state.restored"
|
||||||
|
- "'*filter' in iptables_state.initial_state"
|
||||||
|
- "'*filter' not in iptables_state.restored"
|
||||||
|
- iptables_state.tables.nat is defined
|
||||||
|
- "'-A INPUT -j ACCEPT' in iptables_state.tables.nat"
|
||||||
|
- "'-A INPUT -j ACCEPT' not in iptables_state.restored"
|
||||||
|
- iptables_state.tables.filter is undefined
|
||||||
|
- iptables_state is changed
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "get security, raw and mangle tables states"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: saved
|
||||||
|
table: "{{ item }}"
|
||||||
|
loop:
|
||||||
|
- security
|
||||||
|
- raw
|
||||||
|
- mangle
|
||||||
|
changed_when: false
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "save state (any table)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: saved
|
||||||
|
register: iptables_state
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'filter' in iptables_state.tables"
|
||||||
|
- "'*filter' in iptables_state.saved"
|
||||||
|
- "'mangle' in iptables_state.tables"
|
||||||
|
- "'*mangle' in iptables_state.saved"
|
||||||
|
- "'nat' in iptables_state.tables"
|
||||||
|
- "'*nat' in iptables_state.saved"
|
||||||
|
- "'raw' in iptables_state.tables"
|
||||||
|
- "'*raw' in iptables_state.saved"
|
||||||
|
- "'security' in iptables_state.tables"
|
||||||
|
- "'*security' in iptables_state.saved"
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "save filter table into a test file"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_tests }}"
|
||||||
|
table: filter
|
||||||
|
state: saved
|
||||||
|
|
||||||
|
- name: "add a table header in comments (# *mangle)"
|
||||||
|
lineinfile:
|
||||||
|
path: "{{ iptables_tests }}"
|
||||||
|
line: "# *mangle"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "restore state (table filter, must NOT report a change, no warning)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_tests }}"
|
||||||
|
table: filter
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'*filter' in iptables_state.initial_state"
|
||||||
|
- "'*mangle' in iptables_state.initial_state"
|
||||||
|
- "'*nat' in iptables_state.initial_state"
|
||||||
|
- "'*raw' in iptables_state.initial_state"
|
||||||
|
- "'*security' in iptables_state.initial_state"
|
||||||
|
- "'filter' in iptables_state.tables"
|
||||||
|
- "'mangle' not in iptables_state.tables"
|
||||||
|
- "'nat' not in iptables_state.tables"
|
||||||
|
- "'raw' not in iptables_state.tables"
|
||||||
|
- "'security' not in iptables_state.tables"
|
||||||
|
- "'*filter' in iptables_state.restored"
|
||||||
|
- "'*mangle' not in iptables_state.restored"
|
||||||
|
- "'*nat' not in iptables_state.restored"
|
||||||
|
- "'*raw' not in iptables_state.restored"
|
||||||
|
- "'*security' not in iptables_state.restored"
|
||||||
|
- iptables_state is not changed
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "restore state (any table, must NOT report a change, no warning)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_tests }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "'*filter' in iptables_state.initial_state"
|
||||||
|
- "'*mangle' in iptables_state.initial_state"
|
||||||
|
- "'*nat' in iptables_state.initial_state"
|
||||||
|
- "'*raw' in iptables_state.initial_state"
|
||||||
|
- "'*security' in iptables_state.initial_state"
|
||||||
|
- "'filter' in iptables_state.tables"
|
||||||
|
- "'mangle' in iptables_state.tables"
|
||||||
|
- "'nat' in iptables_state.tables"
|
||||||
|
- "'raw' in iptables_state.tables"
|
||||||
|
- "'security' in iptables_state.tables"
|
||||||
|
- "'*filter' in iptables_state.restored"
|
||||||
|
- "'*mangle' in iptables_state.restored"
|
||||||
|
- "'*nat' in iptables_state.restored"
|
||||||
|
- "'*raw' in iptables_state.restored"
|
||||||
|
- "'*security' in iptables_state.restored"
|
||||||
|
- iptables_state is not changed
|
||||||
|
quiet: yes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "restore state (table mangle, must fail, no warning)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_tests }}"
|
||||||
|
table: mangle
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: "explain expected failure"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
- "iptables_state.msg == 'Table mangle to restore not defined in {{ iptables_tests }}'"
|
||||||
|
success_msg: >-
|
||||||
|
The previous error has been triggered by trying to restore a table
|
||||||
|
that is missing in the file provided to iptables-restore.
|
||||||
|
fail_msg: >-
|
||||||
|
The previous task should have failed due to a missing table (mangle)
|
||||||
|
in the file to restore iptables state from.
|
|
@ -0,0 +1,199 @@
|
||||||
|
---
|
||||||
|
- name: "create a blocking ruleset with a DROP policy"
|
||||||
|
copy:
|
||||||
|
dest: "{{ iptables_tests }}"
|
||||||
|
content: |
|
||||||
|
*filter
|
||||||
|
:INPUT DROP
|
||||||
|
COMMIT
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "restore state from the test file (check_mode, must report a change)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_tests }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
check_mode: yes
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is changed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "fail to restore state from the test file"
|
||||||
|
block:
|
||||||
|
- name: "restore state from the test file (bad policies, expected error -> rollback)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_tests }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: "explain expected failure"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
- not iptables_state.applied
|
||||||
|
success_msg: >-
|
||||||
|
The previous error has been triggered to test the rollback. If you
|
||||||
|
are there, it means that 1) connection has been lost right after the
|
||||||
|
bad rules have been restored; 2) a rollback happened, so the bad
|
||||||
|
rules are not applied, finally; 3) module failed because it didn't
|
||||||
|
reach the wanted state, but at least host is not lost !!!
|
||||||
|
fail_msg: >-
|
||||||
|
The previous error has been triggered but its results are not as
|
||||||
|
expected.
|
||||||
|
|
||||||
|
- name: "check that the expected failure happened"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "fail to restore state from the test file (again)"
|
||||||
|
block:
|
||||||
|
- name: "try again, with a higher timeout (bad policies, same expected error)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_tests }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
vars:
|
||||||
|
ansible_timeout: "{{ max_delay | d(300) }}"
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: "explain expected failure"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
- not iptables_state.applied
|
||||||
|
success_msg: >-
|
||||||
|
The previous error has been triggered to test the rollback. If you
|
||||||
|
are there, it means that 1) connection has been lost right after the
|
||||||
|
bad rules have been restored; 2) a rollback happened, so the bad
|
||||||
|
rules are not applied, finally; 3) module failed because it didn't
|
||||||
|
reach the wanted state, but at least host is not lost !!!
|
||||||
|
fail_msg: >-
|
||||||
|
The previous error has been triggered but its results are not as
|
||||||
|
expected.
|
||||||
|
|
||||||
|
- name: "check that the expected failure happened"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "restore state from backup (must NOT report a change)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "restore state from backup (mangle, must NOT report a change)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_saved }}"
|
||||||
|
table: mangle
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
- name: "assert that results are as expected"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "create a blocking ruleset with a REJECT rule"
|
||||||
|
copy:
|
||||||
|
dest: "{{ iptables_tests }}"
|
||||||
|
content: |
|
||||||
|
*filter
|
||||||
|
-A INPUT -j REJECT
|
||||||
|
COMMIT
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "fail to restore state from the test file (again)"
|
||||||
|
block:
|
||||||
|
- name: "restore state from the test file (bad rules, expected error -> rollback)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_tests }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: "explain expected failure"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
- not iptables_state.applied
|
||||||
|
success_msg: >-
|
||||||
|
The previous error has been triggered to test the rollback. If you
|
||||||
|
are there, it means that 1) connection has been lost right after the
|
||||||
|
bad rules have been restored; 2) a rollback happened, so the bad
|
||||||
|
rules are not applied, finally; 3) module failed because it didn't
|
||||||
|
reach the wanted state, but at least host is not lost !!!
|
||||||
|
fail_msg: >-
|
||||||
|
The previous error has been triggered but its results are not as
|
||||||
|
expected.
|
||||||
|
|
||||||
|
- name: "check that the expected failure happened"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: "fail to restore state from the test file (again)"
|
||||||
|
block:
|
||||||
|
- name: "try again, with a higher timeout (bad rules, same expected error)"
|
||||||
|
iptables_state:
|
||||||
|
path: "{{ iptables_tests }}"
|
||||||
|
state: restored
|
||||||
|
register: iptables_state
|
||||||
|
async: "{{ ansible_timeout }}"
|
||||||
|
poll: 0
|
||||||
|
vars:
|
||||||
|
ansible_timeout: "{{ max_delay | d(300) }}"
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: "explain expected failure"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is not changed
|
||||||
|
- not iptables_state.applied
|
||||||
|
success_msg: >-
|
||||||
|
The previous error has been triggered to test the rollback. If you
|
||||||
|
are there, it means that 1) connection has been lost right after the
|
||||||
|
bad rules have been restored; 2) a rollback happened, so the bad
|
||||||
|
rules are not applied, finally; 3) module failed because it didn't
|
||||||
|
reach the wanted state, but at least host is not lost !!!
|
||||||
|
fail_msg: >-
|
||||||
|
The previous error has been triggered but its results are not as
|
||||||
|
expected.
|
||||||
|
|
||||||
|
- name: "check that the expected failure happened"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- iptables_state is failed
|
|
@ -1224,6 +1224,7 @@ plugins/modules/system/gconftool2.py validate-modules:parameter-state-invalid-ch
|
||||||
plugins/modules/system/gconftool2.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/system/gconftool2.py validate-modules:parameter-type-not-in-doc
|
||||||
plugins/modules/system/interfaces_file.py pylint:blacklisted-name
|
plugins/modules/system/interfaces_file.py pylint:blacklisted-name
|
||||||
plugins/modules/system/interfaces_file.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/system/interfaces_file.py validate-modules:parameter-type-not-in-doc
|
||||||
|
plugins/modules/system/iptables_state.py validate-modules:undocumented-parameter
|
||||||
plugins/modules/system/java_cert.py pylint:blacklisted-name
|
plugins/modules/system/java_cert.py pylint:blacklisted-name
|
||||||
plugins/modules/system/java_keystore.py validate-modules:doc-missing-type
|
plugins/modules/system/java_keystore.py validate-modules:doc-missing-type
|
||||||
plugins/modules/system/java_keystore.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/system/java_keystore.py validate-modules:parameter-type-not-in-doc
|
||||||
|
|
|
@ -1224,6 +1224,7 @@ plugins/modules/system/gconftool2.py validate-modules:parameter-state-invalid-ch
|
||||||
plugins/modules/system/gconftool2.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/system/gconftool2.py validate-modules:parameter-type-not-in-doc
|
||||||
plugins/modules/system/interfaces_file.py pylint:blacklisted-name
|
plugins/modules/system/interfaces_file.py pylint:blacklisted-name
|
||||||
plugins/modules/system/interfaces_file.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/system/interfaces_file.py validate-modules:parameter-type-not-in-doc
|
||||||
|
plugins/modules/system/iptables_state.py validate-modules:undocumented-parameter
|
||||||
plugins/modules/system/java_cert.py pylint:blacklisted-name
|
plugins/modules/system/java_cert.py pylint:blacklisted-name
|
||||||
plugins/modules/system/java_keystore.py validate-modules:doc-missing-type
|
plugins/modules/system/java_keystore.py validate-modules:doc-missing-type
|
||||||
plugins/modules/system/java_keystore.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/system/java_keystore.py validate-modules:parameter-type-not-in-doc
|
||||||
|
|
|
@ -947,6 +947,7 @@ plugins/modules/system/gconftool2.py pylint:blacklisted-name
|
||||||
plugins/modules/system/gconftool2.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/system/gconftool2.py validate-modules:parameter-type-not-in-doc
|
||||||
plugins/modules/system/interfaces_file.py pylint:blacklisted-name
|
plugins/modules/system/interfaces_file.py pylint:blacklisted-name
|
||||||
plugins/modules/system/interfaces_file.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/system/interfaces_file.py validate-modules:parameter-type-not-in-doc
|
||||||
|
plugins/modules/system/iptables_state.py validate-modules:undocumented-parameter
|
||||||
plugins/modules/system/java_cert.py pylint:blacklisted-name
|
plugins/modules/system/java_cert.py pylint:blacklisted-name
|
||||||
plugins/modules/system/java_keystore.py validate-modules:doc-missing-type
|
plugins/modules/system/java_keystore.py validate-modules:doc-missing-type
|
||||||
plugins/modules/system/kernel_blacklist.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/system/kernel_blacklist.py validate-modules:parameter-type-not-in-doc
|
||||||
|
|
Loading…
Reference in a new issue