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
This commit is contained in:
quidame 2020-08-15 08:36:07 +00:00 committed by GitHub
commit 92242d898d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1682 additions and 0 deletions

View 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

View file

@ -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

View file

@ -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.

View file

@ -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