mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
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 <felix@fontein.de> * Update changelogs/fragments/6171-shutdown-using-systemd.yml Co-authored-by: Felix Fontein <felix@fontein.de> * Fix indentation. --------- Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
ca3beb68de
commit
88f7b5a675
5 changed files with 107 additions and 51 deletions
2
changelogs/fragments/6171-shutdown-using-systemd.yml
Normal file
2
changelogs/fragments/6171-shutdown-using-systemd.yml
Normal file
|
@ -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).
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
azp/posix/1
|
||||
destructive
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue