1
0
Fork 0
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:
Peter Upton 2023-05-09 13:10:09 -05:00 committed by GitHub
parent ca3beb68de
commit 88f7b5a675
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 107 additions and 51 deletions

View 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).

View file

@ -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:

View file

@ -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"

View file

@ -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

View file

@ -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"