# Copyright (c) 2022 Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """Tests for the reboot action plugin.""" from __future__ import annotations import os import pytest from ansible.errors import AnsibleConnectionFailure from ansible.playbook.task import Task from ansible.plugins.action.reboot import ActionModule as RebootAction from ansible.plugins.loader import connection_loader @pytest.fixture def task_args(request): """Return playbook task args.""" return getattr(request, 'param', {}) @pytest.fixture def module_task(mocker, task_args): """Construct a task object.""" task = mocker.MagicMock(Task) task.action = 'reboot' task.args = task_args task.async_val = False task.check_mode = False task.diff = False return task @pytest.fixture def play_context(mocker): """Construct a play context.""" ctx = mocker.MagicMock() ctx.check_mode = False ctx.shell = 'sh' return ctx @pytest.fixture def action_plugin(play_context, module_task): """Initialize an action plugin.""" connection = connection_loader.get('local', play_context, os.devnull) loader = None templar = None shared_loader_obj = None return RebootAction( module_task, connection, play_context, loader, templar, shared_loader_obj, ) _SENTINEL_REBOOT_COMMAND = '/reboot-command-mock --arg' _SENTINEL_SHORT_REBOOT_COMMAND = '/reboot-command-mock' _SENTINEL_TEST_COMMAND = 'cmd-stub' @pytest.mark.parametrize( 'task_args', ( { 'reboot_timeout': 5, 'reboot_command': _SENTINEL_REBOOT_COMMAND, 'test_command': _SENTINEL_TEST_COMMAND, }, { 'reboot_timeout': 5, 'reboot_command': _SENTINEL_SHORT_REBOOT_COMMAND, 'test_command': _SENTINEL_TEST_COMMAND, }, ), ids=('reboot command with spaces', 'reboot command without spaces'), indirect=('task_args', ), ) def test_reboot_command(action_plugin, mocker, monkeypatch, task_args): """Check that the reboot command gets called and reboot verified.""" def _patched_low_level_execute_command(cmd, *args, **kwargs): return { _SENTINEL_TEST_COMMAND: { 'rc': 0, 'stderr': '', 'stdout': '', }, _SENTINEL_REBOOT_COMMAND: { 'rc': 0, 'stderr': '', 'stdout': '', }, f'{_SENTINEL_SHORT_REBOOT_COMMAND} ': { # no args is concatenated 'rc': 0, 'stderr': '', 'stdout': '', }, }[cmd] monkeypatch.setattr( action_plugin, '_low_level_execute_command', _patched_low_level_execute_command, ) action_plugin._connection = mocker.Mock() monkeypatch.setattr(action_plugin, 'check_boot_time', lambda *_a, **_kw: 5) monkeypatch.setattr(action_plugin, 'get_distribution', mocker.MagicMock()) monkeypatch.setattr(action_plugin, 'get_system_boot_time', lambda d: 0) low_level_cmd_spy = mocker.spy(action_plugin, '_low_level_execute_command') action_result = action_plugin.run() assert low_level_cmd_spy.called expected_reboot_command = ( task_args['reboot_command'] if ' ' in task_args['reboot_command'] else f'{task_args["reboot_command"] !s} ' ) low_level_cmd_spy.assert_any_call(expected_reboot_command, sudoable=True) low_level_cmd_spy.assert_any_call(task_args['test_command'], sudoable=True) assert low_level_cmd_spy.call_count == 2 assert low_level_cmd_spy.spy_return == { 'rc': 0, 'stderr': '', 'stdout': '', } assert low_level_cmd_spy.spy_exception is None assert 'failed' not in action_result assert action_result == {'rebooted': True, 'changed': True, 'elapsed': 0} @pytest.mark.parametrize( 'task_args', ( { 'reboot_timeout': 5, 'reboot_command': _SENTINEL_REBOOT_COMMAND, 'test_command': _SENTINEL_TEST_COMMAND, }, ), ids=('reboot command with spaces', ), indirect=('task_args', ), ) def test_reboot_command_connection_fail(action_plugin, mocker, monkeypatch, task_args): """Check that the reboot command gets called and reboot verified.""" def _patched_low_level_execute_command(cmd, *args, **kwargs): if cmd == _SENTINEL_REBOOT_COMMAND: raise AnsibleConnectionFailure('Fake connection drop') return { _SENTINEL_TEST_COMMAND: { 'rc': 0, 'stderr': '', 'stdout': '', }, }[cmd] monkeypatch.setattr( action_plugin, '_low_level_execute_command', _patched_low_level_execute_command, ) action_plugin._connection = mocker.Mock() monkeypatch.setattr(action_plugin, 'check_boot_time', lambda *_a, **_kw: 5) monkeypatch.setattr(action_plugin, 'get_distribution', mocker.MagicMock()) monkeypatch.setattr(action_plugin, 'get_system_boot_time', lambda d: 0) low_level_cmd_spy = mocker.spy(action_plugin, '_low_level_execute_command') action_result = action_plugin.run() assert low_level_cmd_spy.called low_level_cmd_spy.assert_any_call( task_args['reboot_command'], sudoable=True, ) low_level_cmd_spy.assert_any_call(task_args['test_command'], sudoable=True) assert low_level_cmd_spy.call_count == 2 assert low_level_cmd_spy.spy_return == { 'rc': 0, 'stderr': '', 'stdout': '', } assert 'failed' not in action_result assert action_result == {'rebooted': True, 'changed': True, 'elapsed': 0} def test_reboot_connection_local(action_plugin, module_task): """Verify that using local connection doesn't let reboot happen.""" expected_message = ' '.join( ( 'Running', module_task.action, 'with local connection would reboot the control node.', ), ) expected_action_result = { 'changed': False, 'elapsed': 0, 'failed': True, 'msg': expected_message, 'rebooted': False, } action_result = action_plugin.run() assert action_result == expected_action_result