diff --git a/lib/ansible/plugins/action/win_reboot.py b/lib/ansible/plugins/action/win_reboot.py new file mode 100644 index 00000000000..ea2021be706 --- /dev/null +++ b/lib/ansible/plugins/action/win_reboot.py @@ -0,0 +1,140 @@ +# (c) 2016, Matt Davis +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# CI-required python3 boilerplate +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.plugins.action import ActionBase +from ansible.utils.boolean import boolean +from ansible.utils.unicode import to_unicode +from ansible.errors import AnsibleUndefinedVariable + +import socket +import time +import traceback + +from datetime import datetime, timedelta + +try: + from __main__ import display +except ImportError: + from ansible.utils.display import Display + display = Display() + +class TimedOutException(Exception): + pass + +class ActionModule(ActionBase): + TRANSFERS_FILES = False + + DEFAULT_SHUTDOWN_TIMEOUT_SEC = 600 + DEFAULT_REBOOT_TIMEOUT_SEC = 600 + DEFAULT_CONNECT_TIMEOUT_SEC = 5 + DEFAULT_PRE_REBOOT_DELAY_SEC = 2 + DEFAULT_TEST_COMMAND = 'whoami' + + def do_until_success_or_timeout(self, what, timeout_sec, what_desc, fail_sleep_sec=1): + max_end_time = datetime.utcnow() + timedelta(seconds=timeout_sec) + + while datetime.utcnow() < max_end_time: + try: + what() + if what_desc: + display.debug("win_reboot: %s success" % what_desc) + return + except: + if what_desc: + display.debug("win_reboot: %s fail (expected), sleeping before retry..." % what_desc) + time.sleep(fail_sleep_sec) + + raise TimedOutException("timed out waiting for %s" % what_desc) + + def run(self, tmp=None, task_vars=None): + if task_vars is None: + task_vars = dict() + + shutdown_timeout_sec = int(self._task.args.get('shutdown_timeout_sec', self.DEFAULT_SHUTDOWN_TIMEOUT_SEC)) + reboot_timeout_sec = int(self._task.args.get('reboot_timeout_sec', self.DEFAULT_REBOOT_TIMEOUT_SEC)) + connect_timeout_sec = int(self._task.args.get('connect_timeout_sec', self.DEFAULT_CONNECT_TIMEOUT_SEC)) + pre_reboot_delay_sec = int(self._task.args.get('pre_reboot_delay_sec', self.DEFAULT_PRE_REBOOT_DELAY_SEC)) + test_command = self._task.args.get('test_command', self.DEFAULT_TEST_COMMAND) + + if self._play_context.check_mode: + display.vvv("win_reboot: skipping for check_mode") + return dict(skipped=True) + + winrm_host = self._connection._winrm_host + winrm_port = self._connection._winrm_port + + result = super(ActionModule, self).run(tmp, task_vars) + + # initiate reboot + (rc, stdout, stderr) = self._connection.exec_command("shutdown /r /t %d" % pre_reboot_delay_sec) + + if rc != 0: + result['failed'] = True + result['rebooted'] = False + result['msg'] = "Shutdown command failed, error text was %s" % stderr + return result + + def raise_if_port_open(): + try: + sock = socket.create_connection((winrm_host, winrm_port), connect_timeout_sec) + sock.close() + except: + return False + + raise Exception("port is open") + + try: + self.do_until_success_or_timeout(raise_if_port_open, shutdown_timeout_sec, what_desc="winrm port down") + + def connect_winrm_port(): + sock = socket.create_connection((winrm_host, winrm_port), connect_timeout_sec) + sock.close() + + self.do_until_success_or_timeout(connect_winrm_port, reboot_timeout_sec, what_desc="winrm port up") + + def run_test_command(): + display.vvv("attempting post-reboot test command '%s'" % test_command) + # call connection reset between runs if it's there + try: + self._connection._reset() + except AttributeError: + pass + + (rc, stdout, stderr) = self._connection.exec_command(test_command) + + if rc != 0: + raise Exception('test command failed') + + # FUTURE: ensure that a reboot has actually occurred by watching for change in last boot time fact + # FUTURE: add a stability check (system must remain up for N seconds) to deal with self-multi-reboot updates + + self.do_until_success_or_timeout(run_test_command, reboot_timeout_sec, what_desc="post-reboot test command success") + + result['rebooted'] = True + result['changed'] = True + + except TimedOutException as toex: + result['failed'] = True + result['rebooted'] = True + result['msg'] = toex.message + + return result + diff --git a/lib/ansible/plugins/connection/winrm.py b/lib/ansible/plugins/connection/winrm.py index 4781188a2b5..57e8c8e3703 100644 --- a/lib/ansible/plugins/connection/winrm.py +++ b/lib/ansible/plugins/connection/winrm.py @@ -213,6 +213,11 @@ class Connection(ConnectionBase): self._connected = True return self + def _reset(self): # used by win_reboot (and any other action that might need to bounce the state) + self.protocol = None + self.shell_id = None + self._connect() + def exec_command(self, cmd, in_data=None, sudoable=True): super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) cmd_parts = shlex.split(to_bytes(cmd), posix=False)