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:
parent
e05c62547b
commit
a51a6f4a25
13 changed files with 201 additions and 135 deletions
|
@ -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)
|
|
@ -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'''
|
||||
|
|
|
@ -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
|
||||
|
|
4
test/integration/targets/reboot/handlers/main.yml
Normal file
4
test/integration/targets/reboot/handlers/main.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
- name: remove molly-guard
|
||||
apt:
|
||||
name: molly-guard
|
||||
state: absent
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
test/integration/targets/reboot/tasks/test_molly_guard.yml
Normal file
20
test/integration/targets/reboot/tasks/test_molly_guard.yml
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue