mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
9226c4b0d5
* 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
)
189 lines
9.1 KiB
Python
189 lines
9.1 KiB
Python
# 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
|