reboot - add reboot_command parameter (#69847)

Fixes #51359
* Update default search paths
* Fix returns for args and command, don't allow conversion
* Reorganize tests
* Fix test for Azure Pipelines
This commit is contained in:
Sam Doran 2020-10-28 12:10:59 -04:00 committed by GitHub
parent e05c62547b
commit a51a6f4a25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 201 additions and 135 deletions

View file

@ -0,0 +1,4 @@
minor_changes:
- >
reboot - add ``reboot_command`` parameter to allow specifying the command
used to reboot the system (https://github.com/ansible/ansible/issues/51359)

View file

@ -60,7 +60,7 @@ options:
- Paths to search on the remote machine for the C(shutdown) command.
- I(Only) these paths will be searched for the C(shutdown) command. C(PATH) is ignored in the remote node when searching for the C(shutdown) command.
type: list
default: ['/sbin', '/usr/sbin', '/usr/local/sbin']
default: ['/sbin', '/bin', '/usr/sbin', '/usr/bin', '/usr/local/sbin']
version_added: '2.8'
boot_time_command:
@ -70,6 +70,16 @@ options:
type: str
default: 'cat /proc/sys/kernel/random/boot_id'
version_added: '2.10'
reboot_command:
description:
- Command to run that reboots the system, including any parameters passed to the command.
- Can be an absolute path to the command or just the command name. If an absolute path to the
command is not given, C(search_paths) on the target system will be searched to find the absolute path.
- This will cause C(pre_reboot_delay), C(post_reboot_delay), and C(msg) to be ignored.
type: str
default: '[determined based on target OS]'
version_added: '2.11'
seealso:
- module: ansible.windows.win_reboot
author:
@ -89,6 +99,12 @@ EXAMPLES = r'''
reboot:
search_paths:
- '/lib/molly-guard'
- name: Reboot machine using a custom reboot command
reboot:
reboot_command: launchctl reboot userspace
boot_time_command: uptime | cut -d ' ' -f 5
'''
RETURN = r'''

View file

@ -12,8 +12,7 @@ from datetime import datetime, timedelta
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.module_utils.common.validation import check_type_str
from ansible.module_utils.common.validation import check_type_list, check_type_str
from ansible.plugins.action import ActionBase
from ansible.utils.display import Display
@ -32,9 +31,10 @@ class ActionModule(ActionBase):
'msg',
'post_reboot_delay',
'pre_reboot_delay',
'test_command',
'reboot_command',
'reboot_timeout',
'search_paths'
'search_paths',
'test_command',
))
DEFAULT_REBOOT_TIMEOUT = 600
@ -114,11 +114,25 @@ class ActionModule(ActionBase):
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_min = self.pre_reboot_delay // 60
reboot_message = self._task.args.get('msg', self.DEFAULT_REBOOT_MESSAGE)
return args.format(delay_sec=self.pre_reboot_delay, delay_min=delay_min, message=reboot_message)
reboot_command = self._task.args.get('reboot_command')
if reboot_command is not None:
try:
reboot_command = check_type_str(reboot_command, allow_conversion=False)
except TypeError as e:
raise AnsibleError("Invalid value given for 'reboot_command': %s." % to_native(e))
# No args were provided
try:
return reboot_command.split(' ', 1)[1]
except IndexError:
return ''
else:
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_min = self.pre_reboot_delay // 60
reboot_message = self._task.args.get('msg', self.DEFAULT_REBOOT_MESSAGE)
return args.format(delay_sec=self.pre_reboot_delay, delay_min=delay_min, message=reboot_message)
def get_distribution(self, task_vars):
# FIXME: only execute the module if we don't already have the facts we need
@ -142,44 +156,49 @@ 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):
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)
reboot_command = self._task.args.get('reboot_command')
if reboot_command is not None:
try:
reboot_command = check_type_str(reboot_command, allow_conversion=False)
except TypeError as e:
raise AnsibleError("Invalid value given for 'reboot_command': %s." % to_native(e))
shutdown_bin = reboot_command.split(' ', 1)[0]
else:
shutdown_bin = self._get_value_from_facts('SHUTDOWN_COMMANDS', distribution, 'DEFAULT_SHUTDOWN_COMMAND')
# 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]
if shutdown_bin[0] == '/':
return shutdown_bin
else:
default_search_paths = ['/sbin', '/bin', '/usr/sbin', '/usr/bin', '/usr/local/sbin']
search_paths = self._task.args.get('search_paths', default_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))
try:
# Convert bare strings to a list
search_paths = check_type_list(search_paths)
except TypeError:
err_msg = "'search_paths' must be a string or flat list of strings, got {0}"
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'
}
)
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))
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
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))
return full_path[0]
def deprecated_args(self):
for arg, version in self.DEPRECATED_ARGS.items():
@ -322,7 +341,7 @@ class ActionModule(ActionBase):
if reboot_result['rc'] != 0:
result['failed'] = True
result['rebooted'] = False
result['msg'] = "Reboot command failed. Error was {stdout}, {stderr}".format(
result['msg'] = "Reboot command failed. Error was: '{stdout}, {stderr}'".format(
stdout=to_native(reboot_result['stdout'].strip()),
stderr=to_native(reboot_result['stderr'].strip()))
return result

View file

@ -0,0 +1,4 @@
- name: remove molly-guard
apt:
name: molly-guard
state: absent

View file

@ -1,5 +1,5 @@
- name: Get current boot time
command: "{{ boot_time_command[ansible_facts['distribution'] | lower] | default('cat /proc/sys/kernel/random/boot_id') }}"
command: "{{ _boot_time_command[ansible_facts['distribution'] | lower] | default('cat /proc/sys/kernel/random/boot_id') }}"
register: after_boot_time
- name: Ensure system was actually rebooted

View file

@ -1,3 +1,3 @@
- name: Get current boot time
command: "{{ boot_time_command[ansible_facts['distribution'] | lower] | default('cat /proc/sys/kernel/random/boot_id') }}"
command: "{{ _boot_time_command[ansible_facts['distribution'] | lower] | default('cat /proc/sys/kernel/random/boot_id') }}"
register: before_boot_time

View file

@ -1,4 +1,6 @@
- block:
- name: Test reboot
when: ansible_facts.virtualization_type | default('') not in ['docker', 'container', 'containerd']
block:
# This block can be removed once we have a mechanism in ansible-test to separate
# the control node from the managed node.
- block:
@ -23,89 +25,17 @@
Skipping reboot test.
that:
- not controller_temp_file.stat.exists
always:
- name: Cleanup temp file
file:
path: /tmp/Anything-Nutlike-Nuzzle-Plow-Overdue
state: absent
delegate_to: localhost
connection: local
when: inventory_hostname == ansible_play_hosts[0]
- import_tasks: get_boot_time.yml
- name: Reboot with default settings
reboot:
register: reboot_result
- import_tasks: check_reboot.yml
- import_tasks: get_boot_time.yml
- name: Reboot with all options
reboot:
connect_timeout: 30
search_paths: /usr/local/bin
msg: Rebooting
post_reboot_delay: 1
pre_reboot_delay: 61
test_command: uptime
reboot_timeout: 500
register: reboot_result
- import_tasks: check_reboot.yml
- import_tasks: get_boot_time.yml
- name: Test with negative values for delays
reboot:
post_reboot_delay: -0.5
pre_reboot_delay: -61
register: reboot_result
- import_tasks: check_reboot.yml
- name: Use invalid parameter
reboot:
foo: bar
ignore_errors: true
register: invalid_parameter
- name: Ensure task fails with error
assert:
that:
- invalid_parameter is failed
- "invalid_parameter.msg == 'Invalid options for reboot: foo'"
- name: Reboot with test command that fails
reboot:
test_command: 'FAIL'
reboot_timeout: "{{ timeout }}"
register: reboot_fail_test
failed_when: "reboot_fail_test.msg != 'Timed out waiting for post-reboot test command (timeout=' ~ timeout ~ ')'"
vars:
timeout: "{{ timeout_value[ansible_facts['distribution'] | lower] | default(60) }}"
- name: Test molly-guard
block:
- import_tasks: get_boot_time.yml
- name: Install molly-guard
apt:
update_cache: yes
name: molly-guard
state: present
- name: Reboot when molly-guard is installed
reboot:
search_paths: /lib/molly-guard
register: reboot_result
- import_tasks: check_reboot.yml
when: ansible_facts.distribution in ['Debian', 'Ubuntu']
tags:
- molly-guard
always:
- name: Cleanup temp file
file:
path: /tmp/Anything-Nutlike-Nuzzle-Plow-Overdue
state: absent
delegate_to: localhost
connection: local
when: inventory_hostname == ansible_play_hosts[0]
when: ansible_virtualization_type | default('') != 'docker'
- import_tasks: test_standard_scenarios.yml
- import_tasks: test_reboot_command.yml
- import_tasks: test_invalid_parameter.yml
- import_tasks: test_invalid_test_command.yml
- import_tasks: test_molly_guard.yml

View file

@ -0,0 +1,11 @@
- name: Use invalid parameter
reboot:
foo: bar
ignore_errors: yes
register: invalid_parameter
- name: Ensure task fails with error
assert:
that:
- invalid_parameter is failed
- "invalid_parameter.msg == 'Invalid options for reboot: foo'"

View file

@ -0,0 +1,8 @@
- name: Reboot with test command that fails
reboot:
test_command: 'FAIL'
reboot_timeout: "{{ timeout }}"
register: reboot_fail_test
failed_when: "reboot_fail_test.msg != 'Timed out waiting for post-reboot test command (timeout=' ~ timeout ~ ')'"
vars:
timeout: "{{ _timeout_value[ansible_facts['distribution'] | lower] | default(60) }}"

View file

@ -0,0 +1,20 @@
- name: Test molly-guard
when: ansible_facts.distribution in ['Debian', 'Ubuntu']
tags:
- molly-guard
block:
- import_tasks: get_boot_time.yml
- name: Install molly-guard
apt:
update_cache: yes
name: molly-guard
state: present
notify: remove molly-guard
- name: Reboot when molly-guard is installed
reboot:
search_paths: /lib/molly-guard
register: reboot_result
- import_tasks: check_reboot.yml

View file

@ -0,0 +1,22 @@
- import_tasks: get_boot_time.yml
- name: Reboot with custom reboot_command using unqualified path
reboot:
reboot_command: reboot
register: reboot_result
- import_tasks: check_reboot.yml
- import_tasks: get_boot_time.yml
- name: Reboot with custom reboot_command using absolute path
reboot:
reboot_command: /sbin/reboot
register: reboot_result
- import_tasks: check_reboot.yml
- import_tasks: get_boot_time.yml
- name: Reboot with custom reboot_command with parameters
reboot:
reboot_command: shutdown -r now
register: reboot_result
- import_tasks: check_reboot.yml

View file

@ -0,0 +1,32 @@
- import_tasks: get_boot_time.yml
- name: Reboot with default settings
reboot:
register: reboot_result
- import_tasks: check_reboot.yml
- import_tasks: get_boot_time.yml
- name: Reboot with all options except reboot_command
reboot:
connect_timeout: 30
search_paths:
- /sbin
- /bin
- /usr/sbin
- /usr/bin
msg: Rebooting
post_reboot_delay: 1
pre_reboot_delay: 61
test_command: uptime
reboot_timeout: 500
register: reboot_result
- import_tasks: check_reboot.yml
- import_tasks: get_boot_time.yml
- name: Test with negative values for delays
reboot:
post_reboot_delay: -0.5
pre_reboot_delay: -61
register: reboot_result
- import_tasks: check_reboot.yml

View file

@ -1,9 +1,9 @@
boot_time_command:
_boot_time_command:
freebsd: '/sbin/sysctl kern.boottime'
openbsd: '/sbin/sysctl kern.boottime'
macosx: 'who -b'
solaris: 'who -b'
sunos: 'who -b'
timeout_value:
_timeout_value:
solaris: 120