summaryrefslogtreecommitdiffstats
path: root/collections-debian-merged/ansible_collections/community/general/plugins/action
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-14 20:03:01 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-14 20:03:01 +0000
commita453ac31f3428614cceb99027f8efbdb9258a40b (patch)
treef61f87408f32a8511cbd91799f9cececb53e0374 /collections-debian-merged/ansible_collections/community/general/plugins/action
parentInitial commit. (diff)
downloadansible-a453ac31f3428614cceb99027f8efbdb9258a40b.tar.xz
ansible-a453ac31f3428614cceb99027f8efbdb9258a40b.zip
Adding upstream version 2.10.7+merged+base+2.10.8+dfsg.upstream/2.10.7+merged+base+2.10.8+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'collections-debian-merged/ansible_collections/community/general/plugins/action')
-rw-r--r--collections-debian-merged/ansible_collections/community/general/plugins/action/__init__.py0
-rw-r--r--collections-debian-merged/ansible_collections/community/general/plugins/action/iptables_state.py198
-rw-r--r--collections-debian-merged/ansible_collections/community/general/plugins/action/shutdown.py211
-rw-r--r--collections-debian-merged/ansible_collections/community/general/plugins/action/system/iptables_state.py198
-rw-r--r--collections-debian-merged/ansible_collections/community/general/plugins/action/system/shutdown.py211
5 files changed, 818 insertions, 0 deletions
diff --git a/collections-debian-merged/ansible_collections/community/general/plugins/action/__init__.py b/collections-debian-merged/ansible_collections/community/general/plugins/action/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/community/general/plugins/action/__init__.py
diff --git a/collections-debian-merged/ansible_collections/community/general/plugins/action/iptables_state.py b/collections-debian-merged/ansible_collections/community/general/plugins/action/iptables_state.py
new file mode 100644
index 00000000..92fb079a
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/community/general/plugins/action/iptables_state.py
@@ -0,0 +1,198 @@
+# 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='ansible.builtin.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']
+ starter_cmd = 'touch %s.starter' % 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":
+ # 1. Catch early errors of the module (in asynchronous task) if any.
+ # Touch a file on the target to signal the module to process now.
+ # 2. Reset connection to ensure a persistent one will not be reused.
+ # 3. 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))
+
+ # The module is aware to not process the main iptables-restore
+ # command before finding (and deleting) the 'starter' cookie on
+ # the host, so the previous query will not reach ssh timeout.
+ garbage = self._low_level_execute_command(starter_cmd, sudoable=self.DEFAULT_SUDOABLE)
+
+ # As the main command is not yet executed on the target, here
+ # 'finished' means 'failed before main command be executed'.
+ if not result['finished']:
+ try:
+ self._connection.reset()
+ except AttributeError:
+ pass
+
+ 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='ansible.builtin.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
diff --git a/collections-debian-merged/ansible_collections/community/general/plugins/action/shutdown.py b/collections-debian-merged/ansible_collections/community/general/plugins/action/shutdown.py
new file mode 100644
index 00000000..e36397ff
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/community/general/plugins/action/shutdown.py
@@ -0,0 +1,211 @@
+# Copyright: (c) 2020, Amin Vakil <info@aminvakil.com>
+# Copyright: (c) 2016-2018, Matt Davis <mdavis@ansible.com>
+# Copyright: (c) 2018, Sam Doran <sdoran@redhat.com>
+# 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
+
+from ansible.errors import AnsibleError, AnsibleConnectionFailure
+from ansible.module_utils._text import to_native, to_text
+from ansible.module_utils.common.collections import is_string
+from ansible.plugins.action import ActionBase
+from ansible.utils.display import Display
+
+display = Display()
+
+
+class TimedOutException(Exception):
+ pass
+
+
+class ActionModule(ActionBase):
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset((
+ 'msg',
+ 'delay',
+ 'search_paths'
+ ))
+
+ DEFAULT_CONNECT_TIMEOUT = None
+ DEFAULT_PRE_SHUTDOWN_DELAY = 0
+ DEFAULT_SHUTDOWN_MESSAGE = 'Shut down initiated by Ansible'
+ DEFAULT_SHUTDOWN_COMMAND = 'shutdown'
+ DEFAULT_SHUTDOWN_COMMAND_ARGS = '-h {delay_min} "{message}"'
+ DEFAULT_SUDOABLE = True
+
+ SHUTDOWN_COMMANDS = {
+ 'alpine': 'poweroff',
+ 'vmkernel': 'halt',
+ }
+
+ SHUTDOWN_COMMAND_ARGS = {
+ 'alpine': '',
+ 'void': '-h +{delay_min} "{message}"',
+ 'freebsd': '-h +{delay_sec}s "{message}"',
+ 'linux': DEFAULT_SHUTDOWN_COMMAND_ARGS,
+ 'macosx': '-h +{delay_min} "{message}"',
+ 'openbsd': '-h +{delay_min} "{message}"',
+ 'solaris': '-y -g {delay_sec} -i 5 "{message}"',
+ 'sunos': '-y -g {delay_sec} -i 5 "{message}"',
+ 'vmkernel': '-d {delay_sec}',
+ 'aix': '-Fh',
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(ActionModule, self).__init__(*args, **kwargs)
+
+ @property
+ def delay(self):
+ return self._check_delay('delay', self.DEFAULT_PRE_SHUTDOWN_DELAY)
+
+ def _check_delay(self, key, default):
+ """Ensure that the value is positive or zero"""
+ value = int(self._task.args.get(key, default))
+ if value < 0:
+ value = 0
+ return value
+
+ def _get_value_from_facts(self, variable_name, distribution, default_value):
+ """Get dist+version specific args first, then distribution, then family, lastly use default"""
+ attr = getattr(self, variable_name)
+ value = attr.get(
+ distribution['name'] + distribution['version'],
+ attr.get(
+ distribution['name'],
+ attr.get(
+ distribution['family'],
+ getattr(self, default_value))))
+ return value
+
+ def get_shutdown_command_args(self, distribution):
+ args = self._get_value_from_facts('SHUTDOWN_COMMAND_ARGS', distribution, 'DEFAULT_SHUTDOWN_COMMAND_ARGS')
+ # Convert seconds to minutes. If less that 60, set it to 0.
+ delay_sec = self.delay
+ shutdown_message = self._task.args.get('msg', self.DEFAULT_SHUTDOWN_MESSAGE)
+ return args.format(delay_sec=delay_sec, delay_min=delay_sec // 60, message=shutdown_message)
+
+ def get_distribution(self, task_vars):
+ # FIXME: only execute the module if we don't already have the facts we need
+ distribution = {}
+ display.debug('{action}: running setup module to get distribution'.format(action=self._task.action))
+ module_output = self._execute_module(
+ task_vars=task_vars,
+ module_name='ansible.legacy.setup',
+ module_args={'gather_subset': 'min'})
+ try:
+ if module_output.get('failed', False):
+ raise AnsibleError('Failed to determine system distribution. {0}, {1}'.format(
+ to_native(module_output['module_stdout']).strip(),
+ to_native(module_output['module_stderr']).strip()))
+ distribution['name'] = module_output['ansible_facts']['ansible_distribution'].lower()
+ distribution['version'] = to_text(module_output['ansible_facts']['ansible_distribution_version'].split('.')[0])
+ distribution['family'] = to_text(module_output['ansible_facts']['ansible_os_family'].lower())
+ display.debug("{action}: distribution: {dist}".format(action=self._task.action, dist=distribution))
+ return distribution
+ except KeyError as ke:
+ raise AnsibleError('Failed to get distribution information. Missing "{0}" in output.'.format(ke.args[0]))
+
+ def get_shutdown_command(self, task_vars, distribution):
+ shutdown_bin = self._get_value_from_facts('SHUTDOWN_COMMANDS', distribution, 'DEFAULT_SHUTDOWN_COMMAND')
+ default_search_paths = ['/sbin', '/usr/sbin', '/usr/local/sbin']
+ search_paths = self._task.args.get('search_paths', default_search_paths)
+
+ # FIXME: switch all this to user arg spec validation methods when they are available
+ # Convert bare strings to a list
+ if is_string(search_paths):
+ search_paths = [search_paths]
+
+ # Error if we didn't get a list
+ err_msg = "'search_paths' must be a string or flat list of strings, got {0}"
+ try:
+ incorrect_type = any(not is_string(x) for x in search_paths)
+ if not isinstance(search_paths, list) or incorrect_type:
+ raise TypeError
+ except TypeError:
+ raise AnsibleError(err_msg.format(search_paths))
+
+ display.debug('{action}: running find module looking in {paths} to get path for "{command}"'.format(
+ action=self._task.action,
+ command=shutdown_bin,
+ paths=search_paths))
+ find_result = self._execute_module(
+ task_vars=task_vars,
+ # prevent collection search by calling with ansible.legacy (still allows library/ override of find)
+ module_name='ansible.legacy.find',
+ module_args={
+ 'paths': search_paths,
+ 'patterns': [shutdown_bin],
+ 'file_type': 'any'
+ }
+ )
+
+ full_path = [x['path'] for x in find_result['files']]
+ if not full_path:
+ raise AnsibleError('Unable to find command "{0}" in search paths: {1}'.format(shutdown_bin, search_paths))
+ self._shutdown_command = full_path[0]
+ return self._shutdown_command
+
+ def perform_shutdown(self, task_vars, distribution):
+ result = {}
+ shutdown_result = {}
+ shutdown_command = self.get_shutdown_command(task_vars, distribution)
+ shutdown_command_args = self.get_shutdown_command_args(distribution)
+ shutdown_command_exec = '{0} {1}'.format(shutdown_command, shutdown_command_args)
+
+ self.cleanup(force=True)
+ try:
+ display.vvv("{action}: shutting down server...".format(action=self._task.action))
+ display.debug("{action}: shutting down server with command '{command}'".format(action=self._task.action, command=shutdown_command_exec))
+ if self._play_context.check_mode:
+ shutdown_result['rc'] = 0
+ else:
+ shutdown_result = self._low_level_execute_command(shutdown_command_exec, sudoable=self.DEFAULT_SUDOABLE)
+ except AnsibleConnectionFailure as e:
+ # If the connection is closed too quickly due to the system being shutdown, carry on
+ display.debug('{action}: AnsibleConnectionFailure caught and handled: {error}'.format(action=self._task.action, error=to_text(e)))
+ shutdown_result['rc'] = 0
+
+ if shutdown_result['rc'] != 0:
+ result['failed'] = True
+ result['shutdown'] = False
+ result['msg'] = "Shutdown command failed. Error was {stdout}, {stderr}".format(
+ stdout=to_native(shutdown_result['stdout'].strip()),
+ stderr=to_native(shutdown_result['stderr'].strip()))
+ return result
+
+ result['failed'] = False
+ result['shutdown_command'] = shutdown_command_exec
+ return result
+
+ def run(self, tmp=None, task_vars=None):
+ self._supports_check_mode = True
+ self._supports_async = True
+
+ # If running with local connection, fail so we don't shutdown ourself
+ if self._connection.transport == 'local' and (not self._play_context.check_mode):
+ msg = 'Running {0} with local connection would shutdown the control node.'.format(self._task.action)
+ return {'changed': False, 'elapsed': 0, 'shutdown': False, 'failed': True, 'msg': msg}
+
+ if task_vars is None:
+ task_vars = {}
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+
+ if result.get('skipped', False) or result.get('failed', False):
+ return result
+
+ distribution = self.get_distribution(task_vars)
+
+ # Initiate shutdown
+ shutdown_result = self.perform_shutdown(task_vars, distribution)
+
+ if shutdown_result['failed']:
+ result = shutdown_result
+ return result
+
+ result['shutdown'] = True
+ result['changed'] = True
+ result['shutdown_command'] = shutdown_result['shutdown_command']
+
+ return result
diff --git a/collections-debian-merged/ansible_collections/community/general/plugins/action/system/iptables_state.py b/collections-debian-merged/ansible_collections/community/general/plugins/action/system/iptables_state.py
new file mode 100644
index 00000000..92fb079a
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/community/general/plugins/action/system/iptables_state.py
@@ -0,0 +1,198 @@
+# 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='ansible.builtin.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']
+ starter_cmd = 'touch %s.starter' % 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":
+ # 1. Catch early errors of the module (in asynchronous task) if any.
+ # Touch a file on the target to signal the module to process now.
+ # 2. Reset connection to ensure a persistent one will not be reused.
+ # 3. 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))
+
+ # The module is aware to not process the main iptables-restore
+ # command before finding (and deleting) the 'starter' cookie on
+ # the host, so the previous query will not reach ssh timeout.
+ garbage = self._low_level_execute_command(starter_cmd, sudoable=self.DEFAULT_SUDOABLE)
+
+ # As the main command is not yet executed on the target, here
+ # 'finished' means 'failed before main command be executed'.
+ if not result['finished']:
+ try:
+ self._connection.reset()
+ except AttributeError:
+ pass
+
+ 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='ansible.builtin.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
diff --git a/collections-debian-merged/ansible_collections/community/general/plugins/action/system/shutdown.py b/collections-debian-merged/ansible_collections/community/general/plugins/action/system/shutdown.py
new file mode 100644
index 00000000..e36397ff
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/community/general/plugins/action/system/shutdown.py
@@ -0,0 +1,211 @@
+# Copyright: (c) 2020, Amin Vakil <info@aminvakil.com>
+# Copyright: (c) 2016-2018, Matt Davis <mdavis@ansible.com>
+# Copyright: (c) 2018, Sam Doran <sdoran@redhat.com>
+# 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
+
+from ansible.errors import AnsibleError, AnsibleConnectionFailure
+from ansible.module_utils._text import to_native, to_text
+from ansible.module_utils.common.collections import is_string
+from ansible.plugins.action import ActionBase
+from ansible.utils.display import Display
+
+display = Display()
+
+
+class TimedOutException(Exception):
+ pass
+
+
+class ActionModule(ActionBase):
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset((
+ 'msg',
+ 'delay',
+ 'search_paths'
+ ))
+
+ DEFAULT_CONNECT_TIMEOUT = None
+ DEFAULT_PRE_SHUTDOWN_DELAY = 0
+ DEFAULT_SHUTDOWN_MESSAGE = 'Shut down initiated by Ansible'
+ DEFAULT_SHUTDOWN_COMMAND = 'shutdown'
+ DEFAULT_SHUTDOWN_COMMAND_ARGS = '-h {delay_min} "{message}"'
+ DEFAULT_SUDOABLE = True
+
+ SHUTDOWN_COMMANDS = {
+ 'alpine': 'poweroff',
+ 'vmkernel': 'halt',
+ }
+
+ SHUTDOWN_COMMAND_ARGS = {
+ 'alpine': '',
+ 'void': '-h +{delay_min} "{message}"',
+ 'freebsd': '-h +{delay_sec}s "{message}"',
+ 'linux': DEFAULT_SHUTDOWN_COMMAND_ARGS,
+ 'macosx': '-h +{delay_min} "{message}"',
+ 'openbsd': '-h +{delay_min} "{message}"',
+ 'solaris': '-y -g {delay_sec} -i 5 "{message}"',
+ 'sunos': '-y -g {delay_sec} -i 5 "{message}"',
+ 'vmkernel': '-d {delay_sec}',
+ 'aix': '-Fh',
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(ActionModule, self).__init__(*args, **kwargs)
+
+ @property
+ def delay(self):
+ return self._check_delay('delay', self.DEFAULT_PRE_SHUTDOWN_DELAY)
+
+ def _check_delay(self, key, default):
+ """Ensure that the value is positive or zero"""
+ value = int(self._task.args.get(key, default))
+ if value < 0:
+ value = 0
+ return value
+
+ def _get_value_from_facts(self, variable_name, distribution, default_value):
+ """Get dist+version specific args first, then distribution, then family, lastly use default"""
+ attr = getattr(self, variable_name)
+ value = attr.get(
+ distribution['name'] + distribution['version'],
+ attr.get(
+ distribution['name'],
+ attr.get(
+ distribution['family'],
+ getattr(self, default_value))))
+ return value
+
+ def get_shutdown_command_args(self, distribution):
+ args = self._get_value_from_facts('SHUTDOWN_COMMAND_ARGS', distribution, 'DEFAULT_SHUTDOWN_COMMAND_ARGS')
+ # Convert seconds to minutes. If less that 60, set it to 0.
+ delay_sec = self.delay
+ shutdown_message = self._task.args.get('msg', self.DEFAULT_SHUTDOWN_MESSAGE)
+ return args.format(delay_sec=delay_sec, delay_min=delay_sec // 60, message=shutdown_message)
+
+ def get_distribution(self, task_vars):
+ # FIXME: only execute the module if we don't already have the facts we need
+ distribution = {}
+ display.debug('{action}: running setup module to get distribution'.format(action=self._task.action))
+ module_output = self._execute_module(
+ task_vars=task_vars,
+ module_name='ansible.legacy.setup',
+ module_args={'gather_subset': 'min'})
+ try:
+ if module_output.get('failed', False):
+ raise AnsibleError('Failed to determine system distribution. {0}, {1}'.format(
+ to_native(module_output['module_stdout']).strip(),
+ to_native(module_output['module_stderr']).strip()))
+ distribution['name'] = module_output['ansible_facts']['ansible_distribution'].lower()
+ distribution['version'] = to_text(module_output['ansible_facts']['ansible_distribution_version'].split('.')[0])
+ distribution['family'] = to_text(module_output['ansible_facts']['ansible_os_family'].lower())
+ display.debug("{action}: distribution: {dist}".format(action=self._task.action, dist=distribution))
+ return distribution
+ except KeyError as ke:
+ raise AnsibleError('Failed to get distribution information. Missing "{0}" in output.'.format(ke.args[0]))
+
+ def get_shutdown_command(self, task_vars, distribution):
+ shutdown_bin = self._get_value_from_facts('SHUTDOWN_COMMANDS', distribution, 'DEFAULT_SHUTDOWN_COMMAND')
+ default_search_paths = ['/sbin', '/usr/sbin', '/usr/local/sbin']
+ search_paths = self._task.args.get('search_paths', default_search_paths)
+
+ # FIXME: switch all this to user arg spec validation methods when they are available
+ # Convert bare strings to a list
+ if is_string(search_paths):
+ search_paths = [search_paths]
+
+ # Error if we didn't get a list
+ err_msg = "'search_paths' must be a string or flat list of strings, got {0}"
+ try:
+ incorrect_type = any(not is_string(x) for x in search_paths)
+ if not isinstance(search_paths, list) or incorrect_type:
+ raise TypeError
+ except TypeError:
+ raise AnsibleError(err_msg.format(search_paths))
+
+ display.debug('{action}: running find module looking in {paths} to get path for "{command}"'.format(
+ action=self._task.action,
+ command=shutdown_bin,
+ paths=search_paths))
+ find_result = self._execute_module(
+ task_vars=task_vars,
+ # prevent collection search by calling with ansible.legacy (still allows library/ override of find)
+ module_name='ansible.legacy.find',
+ module_args={
+ 'paths': search_paths,
+ 'patterns': [shutdown_bin],
+ 'file_type': 'any'
+ }
+ )
+
+ full_path = [x['path'] for x in find_result['files']]
+ if not full_path:
+ raise AnsibleError('Unable to find command "{0}" in search paths: {1}'.format(shutdown_bin, search_paths))
+ self._shutdown_command = full_path[0]
+ return self._shutdown_command
+
+ def perform_shutdown(self, task_vars, distribution):
+ result = {}
+ shutdown_result = {}
+ shutdown_command = self.get_shutdown_command(task_vars, distribution)
+ shutdown_command_args = self.get_shutdown_command_args(distribution)
+ shutdown_command_exec = '{0} {1}'.format(shutdown_command, shutdown_command_args)
+
+ self.cleanup(force=True)
+ try:
+ display.vvv("{action}: shutting down server...".format(action=self._task.action))
+ display.debug("{action}: shutting down server with command '{command}'".format(action=self._task.action, command=shutdown_command_exec))
+ if self._play_context.check_mode:
+ shutdown_result['rc'] = 0
+ else:
+ shutdown_result = self._low_level_execute_command(shutdown_command_exec, sudoable=self.DEFAULT_SUDOABLE)
+ except AnsibleConnectionFailure as e:
+ # If the connection is closed too quickly due to the system being shutdown, carry on
+ display.debug('{action}: AnsibleConnectionFailure caught and handled: {error}'.format(action=self._task.action, error=to_text(e)))
+ shutdown_result['rc'] = 0
+
+ if shutdown_result['rc'] != 0:
+ result['failed'] = True
+ result['shutdown'] = False
+ result['msg'] = "Shutdown command failed. Error was {stdout}, {stderr}".format(
+ stdout=to_native(shutdown_result['stdout'].strip()),
+ stderr=to_native(shutdown_result['stderr'].strip()))
+ return result
+
+ result['failed'] = False
+ result['shutdown_command'] = shutdown_command_exec
+ return result
+
+ def run(self, tmp=None, task_vars=None):
+ self._supports_check_mode = True
+ self._supports_async = True
+
+ # If running with local connection, fail so we don't shutdown ourself
+ if self._connection.transport == 'local' and (not self._play_context.check_mode):
+ msg = 'Running {0} with local connection would shutdown the control node.'.format(self._task.action)
+ return {'changed': False, 'elapsed': 0, 'shutdown': False, 'failed': True, 'msg': msg}
+
+ if task_vars is None:
+ task_vars = {}
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+
+ if result.get('skipped', False) or result.get('failed', False):
+ return result
+
+ distribution = self.get_distribution(task_vars)
+
+ # Initiate shutdown
+ shutdown_result = self.perform_shutdown(task_vars, distribution)
+
+ if shutdown_result['failed']:
+ result = shutdown_result
+ return result
+
+ result['shutdown'] = True
+ result['changed'] = True
+ result['shutdown_command'] = shutdown_result['shutdown_command']
+
+ return result