Repository: ansible/ansible

Issue:
win_reboot is tied to winrm connection plugin
<!---
Please do not report issues/requests related to Ansible modules here !!

Report them to the appropriate modules-core or modules-extras project:
  - https://github.com/ansible/ansible-modules-core/issues
  - https://github.com/ansible/ansible-modules-extras/issues

Also verify first that your issue/request is not already reported in GitHub
-->
##### ISSUE TYPE

<!--- Pick one below and delete the rest: -->
- Bug Report
##### COMPONENT NAME

<!--- Name of the plugin/task/feature -->

win_reboot
##### ANSIBLE VERSION

<!--- Paste verbatim output from “ansible --version” between quotes below -->

```
ansible 2.1.0.0
```
##### CONFIGURATION

<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

retry_files_enabled = False
##### OS / ENVIRONMENT

<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->

N/A
##### SUMMARY

<!--- Explain the problem briefly -->

The win_reboot action incorrectly assumes that the WinRM connection transport is being utilized to speak to the machine, however if someone is running a custom ansible transport, win_reboot assumes that the transport has properties such as `_winrm_host`, `_winrm_port`, and `_reset()`.
##### STEPS TO REPRODUCE

<!---
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

Use a custom transport to talk to a Windows machine and use the win_reboot function (sadly, our transport is not open sourced yet. Coming soon!).

https://github.com/ansible/ansible/blob/6787fc70a643fb6e2bdd2c6a6202072d21db72ef/lib/ansible/plugins/action/win_reboot.py#L79-L80

https://github.com/ansible/ansible/blob/6787fc70a643fb6e2bdd2c6a6202072d21db72ef/lib/ansible/plugins/action/win_reboot.py#L115

<!--- You can also paste gist.github.com links for larger files -->
##### EXPECTED RESULTS

<!--- What did you expect to happen when running the steps above? -->

I would hope we can make it so that an action doesn't implicitly depend on WinRM being the transport.
##### ACTUAL RESULTS

<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

It does implicitly depend on WinRM being the transport. :-)



Discussion:
@fxfitz: Thanks for taking the time to open this issue, but it was filed under the wrong github repository. Please create a new issue at https://github.com/ansible/ansible-modules-core/issues
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_wrong_repo --->

I don't believe this was filed under the wrong github repository, as win_reboot is not a module and is a custom action within the ansible repository (and does not live in the ansible-modules-core or ansible-modules-extra).

Tagging @nitzmahone :-)

bot broken

EDIT: removed bot broken command -jctanner
Yeah, my intent when I built that was to actually add the necessary properties to ConnectionBase, but it was late enough in the game for that release that it didn't happen. I can make the reset() dep conditional the way I did in async_wrapper, and I might be able to reliably get the host/port elsewhere (but it needs to be a delegation-friendly source- don't want to duplicate the delegator var rewriting)...

I have the same issue with **wait_for_connection**  which could leverage a generic connection-agnostic approach, see #20011 
As soon as #20011 is merged, I can implement a fix for this.
@dagwieers You're a gentleman and a scholar, good sir!
Still waiting for #20011 to get merged.

Resolving patch:
diff --git a/lib/ansible/modules/windows/win_reboot.py b/lib/ansible/modules/windows/win_reboot.py
--- a/lib/ansible/modules/windows/win_reboot.py
+++ b/lib/ansible/modules/windows/win_reboot.py
@@ -1,29 +1,12 @@
 #!/usr/bin/python
 # -*- coding: utf-8 -*-
 
-# 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 <http://www.gnu.org/licenses/>.
-
-# this is a windows documentation stub.  actual code lives in the .ps1
-# file of the same name
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
 
 ANSIBLE_METADATA = {'metadata_version': '1.1',
                     'status': ['stableinterface'],
                     'supported_by': 'core'}
 
-
 DOCUMENTATION = r'''
 ---
 module: win_reboot
@@ -36,6 +19,12 @@
     description:
     - Seconds for shutdown to wait before requesting reboot
     default: 2
+  post_reboot_delay_sec:
+    description:
+    - Seconds to wait after the reboot was successful and the connection was re-established
+    - This is useful if you want wait for something to settle despite your connection already working
+    default: 0
+    version_added: '2.4'
   shutdown_timeout_sec:
     description:
     - Maximum seconds to wait for shutdown to occur
@@ -86,4 +75,9 @@
     returned: always
     type: boolean
     sample: true
+elapsed:
+  description: The number of seconds that elapsed waiting for the system to be rebooted.
+  returned: always
+  type: int
+  sample: 23
 '''
diff --git a/lib/ansible/plugins/action/wait_for_connection.py b/lib/ansible/plugins/action/wait_for_connection.py
--- a/lib/ansible/plugins/action/wait_for_connection.py
+++ b/lib/ansible/plugins/action/wait_for_connection.py
@@ -22,7 +22,6 @@
 import time
 from datetime import datetime, timedelta
 
-from ansible.module_utils.pycompat24 import get_exception
 from ansible.plugins.action import ActionBase
 
 try:
@@ -47,19 +46,19 @@ class ActionModule(ActionBase):
     def do_until_success_or_timeout(self, what, timeout, connect_timeout, what_desc, sleep=1):
         max_end_time = datetime.utcnow() + timedelta(seconds=timeout)
 
+        e = None
         while datetime.utcnow() < max_end_time:
             try:
                 what(connect_timeout)
                 if what_desc:
                     display.debug("wait_for_connection: %s success" % what_desc)
                 return
-            except Exception:
-                e = get_exception()
+            except Exception as e:
                 if what_desc:
                     display.debug("wait_for_connection: %s fail (expected), retrying in %d seconds..." % (what_desc, sleep))
                 time.sleep(sleep)
 
-        raise TimedOutException("timed out waiting for %s: %s" % (what_desc, str(e)))
+        raise TimedOutException("timed out waiting for %s: %s" % (what_desc, e))
 
     def run(self, tmp=None, task_vars=None):
         if task_vars is None:
@@ -108,8 +107,7 @@ def ping_module_test(connect_timeout):
             # Use the ping module test to determine end-to-end connectivity
             self.do_until_success_or_timeout(ping_module_test, timeout, connect_timeout, what_desc="ping module test success", sleep=sleep)
 
-        except TimedOutException:
-            e = get_exception()
+        except TimedOutException as e:
             result['failed'] = True
             result['msg'] = str(e)
 
diff --git a/lib/ansible/plugins/action/win_reboot.py b/lib/ansible/plugins/action/win_reboot.py
--- a/lib/ansible/plugins/action/win_reboot.py
+++ b/lib/ansible/plugins/action/win_reboot.py
@@ -1,21 +1,6 @@
 # (c) 2016, Matt Davis <mdavis@ansible.com>
-#
-# 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 <http://www.gnu.org/licenses/>.
-
-# CI-required python3 boilerplate
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
 from __future__ import (absolute_import, division, print_function)
 __metaclass__ = type
 
@@ -44,52 +29,60 @@ class ActionModule(ActionBase):
     DEFAULT_REBOOT_TIMEOUT_SEC = 600
     DEFAULT_CONNECT_TIMEOUT_SEC = 5
     DEFAULT_PRE_REBOOT_DELAY_SEC = 2
+    DEFAULT_POST_REBOOT_DELAY_SEC = 0
     DEFAULT_TEST_COMMAND = 'whoami'
     DEFAULT_REBOOT_MESSAGE = 'Reboot initiated by Ansible.'
 
     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)
 
+        e = None
         while datetime.utcnow() < max_end_time:
             try:
                 what()
                 if what_desc:
                     display.debug("win_reboot: %s success" % what_desc)
                 return
-            except:
+            except Exception as e:
                 if what_desc:
-                    display.debug("win_reboot: %s fail (expected), sleeping before retry..." % what_desc)
+                    display.debug("win_reboot: %s fail (expected), retrying in %d seconds..." % (what_desc, fail_sleep_sec))
                 time.sleep(fail_sleep_sec)
 
-        raise TimedOutException("timed out waiting for %s" % what_desc)
+        raise TimedOutException("timed out waiting for %s: %s" % (what_desc, e))
 
     def run(self, tmp=None, task_vars=None):
+
+        self._supports_check_mode = True
+        self._supports_async = True
+
+        if self._play_context.check_mode:
+            return dict(changed=True, elapsed=0, rebooted=True)
+
         if task_vars is None:
             task_vars = dict()
 
+        result = super(ActionModule, self).run(tmp, task_vars)
+
+        if result.get('skipped', False) or result.get('failed', False):
+            return result
+
+        winrm_host = self._connection._winrm_host
+        winrm_port = self._connection._winrm_port
+
         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))
+        post_reboot_delay_sec = int(self._task.args.get('post_reboot_delay_sec', self.DEFAULT_POST_REBOOT_DELAY_SEC))
         test_command = str(self._task.args.get('test_command', self.DEFAULT_TEST_COMMAND))
         msg = str(self._task.args.get('msg', self.DEFAULT_REBOOT_MESSAGE))
 
-        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)
-        result['warnings'] = []
-
         # Initiate reboot
         (rc, stdout, stderr) = self._connection.exec_command('shutdown /r /t %d /c "%s"' % (pre_reboot_delay_sec, msg))
 
         # Test for "A system shutdown has already been scheduled. (1190)" and handle it gracefully
         if rc == 1190:
-            result['warnings'].append('A scheduled reboot was pre-empted by Ansible.')
+            display.warning('A scheduled reboot was pre-empted by Ansible.')
 
             # Try to abort (this may fail if it was already aborted)
             (rc, stdout1, stderr1) = self._connection.exec_command('shutdown /a')
@@ -114,6 +107,8 @@ def raise_if_port_open():
 
             raise Exception("port is open")
 
+        start = datetime.now()
+
         try:
             self.do_until_success_or_timeout(raise_if_port_open, shutdown_timeout_sec, what_desc="winrm port down")
 
@@ -149,4 +144,11 @@ def run_test_command():
             result['rebooted'] = True
             result['msg'] = toex.message
 
+        if post_reboot_delay_sec != 0:
+            display.vvv("win_reboot: waiting an additional %d seconds" % post_reboot_delay_sec)
+            time.sleep(post_reboot_delay_sec)
+
+        elapsed = datetime.now() - start
+        result['elapsed'] = elapsed.seconds
+
         return result


Test patch: