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
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure
|
from ansible.errors import AnsibleError, AnsibleConnectionFailure
|
||||||
|
@ -80,13 +81,6 @@ class ActionModule(ActionBase):
|
||||||
getattr(self, default_value))))
|
getattr(self, default_value))))
|
||||||
return 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):
|
def get_distribution(self, task_vars):
|
||||||
# FIXME: only execute the module if we don't already have the facts we need
|
# FIXME: only execute the module if we don't already have the facts we need
|
||||||
distribution = {}
|
distribution = {}
|
||||||
|
@ -101,7 +95,8 @@ class ActionModule(ActionBase):
|
||||||
to_native(module_output['module_stdout']).strip(),
|
to_native(module_output['module_stdout']).strip(),
|
||||||
to_native(module_output['module_stderr']).strip()))
|
to_native(module_output['module_stderr']).strip()))
|
||||||
distribution['name'] = module_output['ansible_facts']['ansible_distribution'].lower()
|
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())
|
distribution['family'] = to_text(module_output['ansible_facts']['ansible_os_family'].lower())
|
||||||
display.debug("{action}: distribution: {dist}".format(action=self._task.action, dist=distribution))
|
display.debug("{action}: distribution: {dist}".format(action=self._task.action, dist=distribution))
|
||||||
return 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]))
|
raise AnsibleError('Failed to get distribution information. Missing "{0}" in output.'.format(ke.args[0]))
|
||||||
|
|
||||||
def get_shutdown_command(self, task_vars, distribution):
|
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')
|
shutdown_bin = self._get_value_from_facts('SHUTDOWN_COMMANDS', distribution, 'DEFAULT_SHUTDOWN_COMMAND')
|
||||||
default_search_paths = ['/sbin', '/usr/sbin', '/usr/local/sbin']
|
default_search_paths = ['/sbin', '/usr/sbin', '/usr/local/sbin']
|
||||||
search_paths = self._task.args.get('search_paths', default_search_paths)
|
search_paths = self._task.args.get('search_paths', default_search_paths)
|
||||||
|
@ -127,45 +139,53 @@ class ActionModule(ActionBase):
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise AnsibleError(err_msg.format(search_paths))
|
raise AnsibleError(err_msg.format(search_paths))
|
||||||
|
|
||||||
display.debug('{action}: running find module looking in {paths} to get path for "{command}"'.format(
|
full_path = find_command(shutdown_bin, search_paths) # find the path to the shutdown command
|
||||||
action=self._task.action,
|
if not full_path: # if we could not find the shutdown command
|
||||||
command=shutdown_bin,
|
display.vvv('Unable to find command "{0}" in search paths: {1}, will attempt a shutdown using systemd '
|
||||||
paths=search_paths))
|
'directly.'.format(shutdown_bin, search_paths)) # tell the user we will try with systemd
|
||||||
find_result = self._execute_module(
|
systemctl_search_paths = ['/bin', '/usr/bin']
|
||||||
task_vars=task_vars,
|
full_path = find_command('systemctl', systemctl_search_paths) # find the path to the systemctl command
|
||||||
# prevent collection search by calling with ansible.legacy (still allows library/ override of find)
|
if not full_path: # if we couldn't find systemctl
|
||||||
module_name='ansible.legacy.find',
|
raise AnsibleError(
|
||||||
module_args={
|
'Could not find command "{0}" in search paths: {1} or systemctl command in search paths: {2}, unable to shutdown.'.
|
||||||
'paths': search_paths,
|
format(shutdown_bin, search_paths, systemctl_search_paths)) # we give up here
|
||||||
'patterns': [shutdown_bin],
|
else:
|
||||||
'file_type': 'any'
|
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']]
|
# systemd case taken care of, here we add args to the command
|
||||||
if not full_path:
|
args = self._get_value_from_facts('SHUTDOWN_COMMAND_ARGS', distribution, 'DEFAULT_SHUTDOWN_COMMAND_ARGS')
|
||||||
raise AnsibleError('Unable to find command "{0}" in search paths: {1}'.format(shutdown_bin, search_paths))
|
# Convert seconds to minutes. If less that 60, set it to 0.
|
||||||
self._shutdown_command = full_path[0]
|
delay_sec = self.delay
|
||||||
return self._shutdown_command
|
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):
|
def perform_shutdown(self, task_vars, distribution):
|
||||||
result = {}
|
result = {}
|
||||||
shutdown_result = {}
|
shutdown_result = {}
|
||||||
shutdown_command = self.get_shutdown_command(task_vars, distribution)
|
shutdown_command_exec = 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)
|
self.cleanup(force=True)
|
||||||
try:
|
try:
|
||||||
display.vvv("{action}: shutting down server...".format(action=self._task.action))
|
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:
|
if self._play_context.check_mode:
|
||||||
shutdown_result['rc'] = 0
|
shutdown_result['rc'] = 0
|
||||||
else:
|
else:
|
||||||
shutdown_result = self._low_level_execute_command(shutdown_command_exec, sudoable=self.DEFAULT_SUDOABLE)
|
shutdown_result = self._low_level_execute_command(shutdown_command_exec, sudoable=self.DEFAULT_SUDOABLE)
|
||||||
except AnsibleConnectionFailure as e:
|
except AnsibleConnectionFailure as e:
|
||||||
# If the connection is closed too quickly due to the system being shutdown, carry on
|
# 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
|
shutdown_result['rc'] = 0
|
||||||
|
|
||||||
if shutdown_result['rc'] != 0:
|
if shutdown_result['rc'] != 0:
|
||||||
|
|
|
@ -14,6 +14,8 @@ short_description: Shut down a machine
|
||||||
notes:
|
notes:
|
||||||
- C(PATH) is ignored on the remote node when searching for the C(shutdown) command. Use I(search_paths)
|
- 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.
|
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:
|
description:
|
||||||
- Shut downs a machine.
|
- Shut downs a machine.
|
||||||
version_added: "1.1.0"
|
version_added: "1.1.0"
|
||||||
|
|
|
@ -3,3 +3,4 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
azp/posix/1
|
azp/posix/1
|
||||||
|
destructive
|
||||||
|
|
|
@ -7,13 +7,7 @@
|
||||||
# Copyright (c) Ansible Project
|
# 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)
|
# 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
|
# 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
|
- name: Execute shutdown with custom message and delay
|
||||||
community.general.shutdown:
|
community.general.shutdown:
|
||||||
|
@ -34,29 +28,44 @@
|
||||||
- '"Custom Message" in shutdown_result["shutdown_command"]'
|
- '"Custom Message" in shutdown_result["shutdown_command"]'
|
||||||
- '"Shut down initiated by Ansible" in shutdown_result_minus["shutdown_command"]'
|
- '"Shut down initiated by Ansible" in shutdown_result_minus["shutdown_command"]'
|
||||||
- '"Custom Message" not 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:
|
assert:
|
||||||
that: '"shutdown" in shutdown_result["shutdown_command"]'
|
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:
|
assert:
|
||||||
that: '"poweroff" in shutdown_result["shutdown_command"]'
|
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:
|
assert:
|
||||||
that: '"halt" in shutdown_result["shutdown_command"]'
|
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:
|
assert:
|
||||||
that:
|
that:
|
||||||
- '"-h 1" in shutdown_result["shutdown_command"]'
|
- '"-h 1" in shutdown_result["shutdown_command"]'
|
||||||
- '"-h 0" in shutdown_result_minus["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
|
- name: Verify shutdown delay is present in minutes in Void, MacOSX, OpenBSD
|
||||||
assert:
|
assert:
|
||||||
|
@ -86,8 +95,30 @@
|
||||||
- '"-d 0" in shutdown_result_minus["shutdown_command"]'
|
- '"-d 0" in shutdown_result_minus["shutdown_command"]'
|
||||||
when: ansible_system == 'VMKernel'
|
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:
|
apt:
|
||||||
name: systemd-sysv
|
name: systemd-sysv
|
||||||
state: absent
|
state: present
|
||||||
when: systemd_sysv_install is changed
|
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