From 88f7b5a6755c51f0d5e14d663ac08cff8fc9934b Mon Sep 17 00:00:00 2001 From: Peter Upton Date: Tue, 9 May 2023 13:10:09 -0500 Subject: [PATCH] Shutdown systemd without sysv (#6171) * setup test * inital working commit to enable shutdown using systemd * add changelog fragment * address sanity checks * fix changelog fragement * update to combine args and command * fix self pararm * fix pylint output * cleanup test * fix tests * fix systemd missing failure message * broaden test coverage * address pr feedback * address sanity test results * fix tests * fix tests * pep8 sanity fix * fix test conditional ordering * quick fix for pep8 * Update changelogs/fragments/6171-shutdown-using-systemd.yml Co-authored-by: Felix Fontein * Update changelogs/fragments/6171-shutdown-using-systemd.yml Co-authored-by: Felix Fontein * Fix indentation. --------- Co-authored-by: Felix Fontein --- .../fragments/6171-shutdown-using-systemd.yml | 2 + plugins/action/shutdown.py | 84 ++++++++++++------- plugins/modules/shutdown.py | 2 + tests/integration/targets/shutdown/aliases | 1 + .../targets/shutdown/tasks/main.yml | 69 ++++++++++----- 5 files changed, 107 insertions(+), 51 deletions(-) create mode 100644 changelogs/fragments/6171-shutdown-using-systemd.yml diff --git a/changelogs/fragments/6171-shutdown-using-systemd.yml b/changelogs/fragments/6171-shutdown-using-systemd.yml new file mode 100644 index 0000000000..e5af7c937c --- /dev/null +++ b/changelogs/fragments/6171-shutdown-using-systemd.yml @@ -0,0 +1,2 @@ +minor_changes: + - shutdown - if no shutdown commands are found in the ``search_paths`` then the module will attempt to shutdown the system using ``systemctl shutdown`` (https://github.com/ansible-collections/community.general/issues/4269, https://github.com/ansible-collections/community.general/pull/6171). diff --git a/plugins/action/shutdown.py b/plugins/action/shutdown.py index c2860f1d6f..914aede393 100644 --- a/plugins/action/shutdown.py +++ b/plugins/action/shutdown.py @@ -6,6 +6,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import (absolute_import, division, print_function) + __metaclass__ = type from ansible.errors import AnsibleError, AnsibleConnectionFailure @@ -80,13 +81,6 @@ class ActionModule(ActionBase): 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 = {} @@ -101,7 +95,8 @@ class ActionModule(ActionBase): 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['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 @@ -109,6 +104,23 @@ class ActionModule(ActionBase): raise AnsibleError('Failed to get distribution information. Missing "{0}" in output.'.format(ke.args[0])) def get_shutdown_command(self, task_vars, distribution): + def find_command(command, find_search_paths): + display.debug('{action}: running find module looking in {paths} to get path for "{command}"'.format( + action=self._task.action, + command=command, + paths=find_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': find_search_paths, + 'patterns': [command], + 'file_type': 'any' + } + ) + return [x['path'] for x in find_result['files']] + 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) @@ -127,45 +139,53 @@ class ActionModule(ActionBase): 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 = find_command(shutdown_bin, search_paths) # find the path to the shutdown command + if not full_path: # if we could not find the shutdown command + display.vvv('Unable to find command "{0}" in search paths: {1}, will attempt a shutdown using systemd ' + 'directly.'.format(shutdown_bin, search_paths)) # tell the user we will try with systemd + systemctl_search_paths = ['/bin', '/usr/bin'] + full_path = find_command('systemctl', systemctl_search_paths) # find the path to the systemctl command + if not full_path: # if we couldn't find systemctl + raise AnsibleError( + 'Could not find command "{0}" in search paths: {1} or systemctl command in search paths: {2}, unable to shutdown.'. + format(shutdown_bin, search_paths, systemctl_search_paths)) # we give up here + else: + return "{0} poweroff".format(full_path[0]) # done, since we cannot use args with systemd shutdown - 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 + # systemd case taken care of, here we add args to the command + 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 '{0} {1}'. \ + format( + full_path[0], + args.format( + delay_sec=delay_sec, + delay_min=delay_sec // 60, + message=shutdown_message + ) + ) 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) + shutdown_command_exec = self.get_shutdown_command(task_vars, distribution) 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)) + 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))) + 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: diff --git a/plugins/modules/shutdown.py b/plugins/modules/shutdown.py index 5d66fad16b..3132af3426 100644 --- a/plugins/modules/shutdown.py +++ b/plugins/modules/shutdown.py @@ -14,6 +14,8 @@ short_description: Shut down a machine notes: - C(PATH) is ignored on the remote node when searching for the C(shutdown) command. Use I(search_paths) to specify locations to search if the default paths do not work. + - The I(msg) and I(delay) options are not supported when a shutdown command is not found in I(search_paths), instead + the module will attempt to shutdown the system by calling C(systemctl shutdown). description: - Shut downs a machine. version_added: "1.1.0" diff --git a/tests/integration/targets/shutdown/aliases b/tests/integration/targets/shutdown/aliases index afda346c4e..428e8289dc 100644 --- a/tests/integration/targets/shutdown/aliases +++ b/tests/integration/targets/shutdown/aliases @@ -3,3 +3,4 @@ # SPDX-License-Identifier: GPL-3.0-or-later azp/posix/1 +destructive diff --git a/tests/integration/targets/shutdown/tasks/main.yml b/tests/integration/targets/shutdown/tasks/main.yml index dadeb62699..f323b8c441 100644 --- a/tests/integration/targets/shutdown/tasks/main.yml +++ b/tests/integration/targets/shutdown/tasks/main.yml @@ -7,13 +7,7 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - -- name: Install systemd-sysv on Ubuntu 18 and Debian - apt: - name: systemd-sysv - state: present - when: (ansible_distribution == 'Ubuntu' and ansible_distribution_major_version is version('18', '>=')) or (ansible_distribution == 'Debian') - register: systemd_sysv_install +# - name: Execute shutdown with custom message and delay community.general.shutdown: @@ -34,29 +28,44 @@ - '"Custom Message" in shutdown_result["shutdown_command"]' - '"Shut down initiated by Ansible" in shutdown_result_minus["shutdown_command"]' - '"Custom Message" not in shutdown_result_minus["shutdown_command"]' - when: ansible_os_family not in ['Alpine', 'AIX'] + when: + - 'ansible_os_family not in ["Alpine", "AIX"]' + - '"systemctl" not in shutdown_result["shutdown_command"]' + - '"systemctl" not in shutdown_result_minus["shutdown_command"]' -- name: Verify shutdown command is present except Alpine, VMKernel +- name: Verify shutdown command is present except Alpine or AIX or systemd assert: that: '"shutdown" in shutdown_result["shutdown_command"]' - when: ansible_os_family != 'Alpine' and ansible_system != 'VMKernel' + when: + - "ansible_os_family != 'Alpine'" + - "ansible_system != 'VMKernel'" + - '"systemctl" not in shutdown_result["shutdown_command"]' -- name: Verify shutdown command is present in Alpine +- name: Verify shutdown command is present in Alpine except systemd assert: that: '"poweroff" in shutdown_result["shutdown_command"]' - when: ansible_os_family == 'Alpine' + when: + - "ansible_os_family == 'Alpine'" + - '"systemctl" not in shutdown_result["shutdown_command"]' -- name: Verify shutdown command is present in VMKernel + +- name: Verify shutdown command is present in VMKernel except systemd assert: that: '"halt" in shutdown_result["shutdown_command"]' - when: ansible_system == 'VMKernel' + when: + - "ansible_system == 'VMKernel'" + - '"systemctl" not in shutdown_result["shutdown_command"]' -- name: Verify shutdown delay is present in minutes in Linux +- name: Verify shutdown delay is present in minutes in Linux except systemd assert: that: - '"-h 1" in shutdown_result["shutdown_command"]' - '"-h 0" in shutdown_result_minus["shutdown_command"]' - when: ansible_system == 'Linux' and ansible_os_family != 'Alpine' + when: + - "ansible_system == 'Linux'" + - "ansible_os_family != 'Alpine'" + - '"systemctl" not in shutdown_result["shutdown_command"]' + - '"systemctl" not in shutdown_result_minus["shutdown_command"]' - name: Verify shutdown delay is present in minutes in Void, MacOSX, OpenBSD assert: @@ -86,8 +95,30 @@ - '"-d 0" in shutdown_result_minus["shutdown_command"]' when: ansible_system == 'VMKernel' -- name: Remove systemd-sysv in ubuntu 18 in case it has been installed in test +- name: Ensure that systemd-sysv is absent in Ubuntu 18 and Debian + apt: + name: sytemd-sysv + state: absent + when: (ansible_distribution == 'Ubuntu' and ansible_distribution_major_version is version('18', '>=')) or (ansible_distribution == 'Debian') + register: systemd_sysv_install + +- name: Gather package facts + package_facts: + manager: apt + when: (ansible_distribution == 'Ubuntu' and ansible_distribution_major_version is version('18', '>=')) or (ansible_distribution == 'Debian') + +- name: Execute shutdown if no systemd-sysv + community.general.shutdown: + register: shutdown_result + check_mode: true + when: + - "(ansible_distribution == 'Ubuntu' and ansible_distribution_major_version is version('18', '>=')) or (ansible_distribution == 'Debian')" + - '"systemd-sysv" not in ansible_facts.packages' + +- name: Install systemd_sysv in case it has been removed in test apt: name: systemd-sysv - state: absent - when: systemd_sysv_install is changed + state: present + when: + - "(ansible_distribution == 'Ubuntu' and ansible_distribution_major_version is version('18', '>=')) or (ansible_distribution == 'Debian')" + - "systemd_sysv_install is changed"