Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reboot - add reboot_command parameter #69847

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelogs/fragments/reboot-add-boot-command-parameter.yaml
@@ -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)
18 changes: 17 additions & 1 deletion lib/ansible/modules/reboot.py
Expand Up @@ -60,7 +60,7 @@
- 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:
Expand All @@ -70,6 +70,16 @@
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:
Expand All @@ -89,6 +99,12 @@
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'''
Expand Down
111 changes: 65 additions & 46 deletions lib/ansible/plugins/action/reboot.py
Expand Up @@ -12,8 +12,7 @@

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

Expand All @@ -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
Expand Down Expand Up @@ -114,11 +114,25 @@ def _get_value_from_facts(self, variable_name, distribution, 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_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
Expand All @@ -142,44 +156,49 @@ def get_distribution(self, task_vars):
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))

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 = [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
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'
}
)

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():
Expand Down Expand Up @@ -322,7 +341,7 @@ def perform_reboot(self, task_vars, distribution):
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
Expand Down
4 changes: 4 additions & 0 deletions test/integration/targets/reboot/handlers/main.yml
@@ -0,0 +1,4 @@
- name: remove molly-guard
apt:
name: molly-guard
state: absent
2 changes: 1 addition & 1 deletion test/integration/targets/reboot/tasks/check_reboot.yml
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/integration/targets/reboot/tasks/get_boot_time.yml
@@ -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
102 changes: 16 additions & 86 deletions test/integration/targets/reboot/tasks/main.yml
@@ -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:
Expand All @@ -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
11 changes: 11 additions & 0 deletions test/integration/targets/reboot/tasks/test_invalid_parameter.yml
@@ -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'"
@@ -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) }}"
20 changes: 20 additions & 0 deletions test/integration/targets/reboot/tasks/test_molly_guard.yml
@@ -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
22 changes: 22 additions & 0 deletions test/integration/targets/reboot/tasks/test_reboot_command.yml
@@ -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