diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-14 20:03:01 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-14 20:03:01 +0000 |
commit | a453ac31f3428614cceb99027f8efbdb9258a40b (patch) | |
tree | f61f87408f32a8511cbd91799f9cececb53e0374 /collections-debian-merged/ansible_collections/community/general/plugins/action | |
parent | Initial commit. (diff) | |
download | ansible-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')
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 |