Repository: ansible/ansible

Issue:
Proper reboot module as action plugin
<!--- Verify first that your issue/request is not already reported in GitHub -->
##### ISSUE TYPE

<!--- Pick one below and delete the rest: -->
- Feature Idea

##### COMPONENT NAME
reboot

##### ANSIBLE VERSION
v2.5

##### SUMMARY

<!--- Explain the problem briefly -->

It would be very useful if Ansible shipped a proper `reboot` module that works for all operating systems. If you see what people have to do today to reboot systems it is clear a better implementation (that works for every operating system) is in order.

The action-plugin would have the following options:
- A delay to give some time to the user(s) to act
- A timeout to ensure that if the reboot has failed Ansible escalates (timeout does not include delay-time)
- A notification message for the user(s)
- Possibly regathering the system facts after the reboot
- A proper check to see if the system has in fact rebooted (check uptime fact)

Various issues have been reported wrt. reboot functionality:
- [If managed system reboots, shell module does not return rc value](https://github.com/ansible/ansible/issues/15741)
- [Reboot and Wait for](https://github.com/ansible/ansible/issues/14413)
- [wait_for: Python exception "socket.error" thrown with search_regex](https://github.com/ansible/ansible/issues/14675)
- [play stopped after reboot in RHEL/CentOS 7](https://github.com/ansible/ansible/issues/9632)
- [Issuing reboot command with sudo fails](https://github.com/ansible/ansible/issues/7124)
- [System reboot module](https://github.com/ansible/ansible/issues/5785)

And various articles have been written regarding the catches, each with their own tricks/advice:
- [Reboot a server and wait for it to come back](https://support.ansible.com/hc/en-us/articles/201958037-Reboot-a-server-and-wait-for-it-to-come-back)
- [How to wait for server restart using Ansible?](http://stackoverflow.com/questions/23877781/how-to-wait-for-server-restart-using-ansible)
- [Ansible: Reboot server in play-book and wait for it come back.](https://www.linux.com/blog/ansible-reboot-server-play-book-and-wait-it-come-back)
- [Rebooting server(s) using Ansible](https://oguya.ch/posts/2015-02-22-ansible-reboot-servers/)
- [Using Ansible to Reboot or Shut Down Devices Running Junos OS](http://www.juniper.net/techpubs/en_US/junos-ansible1.0/topics/example/junos-ansible-playbooks-device-rebooting.html)
- [Rebooting Servers One-by-One With Ansible](http://elnur.pro/rebooting-servers-one-by-one-with-ansible/)



Discussion:
cc @nitzmahone

I would expand it to a 'power' module that also does on/of, not sure if anyone wants to mess with runlevels, but that might also fit 

Right, I was wondering about that, however power on/off is less clear-cut.
- if the system is turned off how is Ansible supposed to continue the playbook ?
- power-on often requires special hardware, or hypervisor support (or wake-on-lan)
- hibernation and sleep are also possibilities

But I don't think these represent common use-cases, whereas rebooting is a common practice to activate security patches, or in some cases to have a working system again ;-) That is why I left other "power-states" out of it.

Yeah, I built both Windows and *nix versions of this action a long time ago for a customer- finally polished  up the Windows one and shipped for 2.1, but I'm still not thrilled with the implementation. I considered trying to make it OS/connection agnostic, and for the most part, it probably could be...

The hardest parts about doing this for *nix are that reboot/shutdown command options seem to differ by OS/distro, and that the command-line reboot delay options aren't very granular. Typically it's either immediate or a minimum delay of 1min. Immediate usually causes a "rude abort" on the connection (less of an issue when it's wrapped in an action), and a 1m delay is pretty excessive when you're talking about fast virtual stuff that can be back up in a matter of seconds. Maybe we could come up with a generic way to trigger the reboot from Python that would give us consistent options

If we do decide to build this, I'd vote to keep it separate from "power". I'm not usually a fan of "all-singing, all-dancing" modules, and as you've already mentioned, the mechanics involved are very different (beyond the "wait for the host" part).

Anyway, +1 from me... There are fewer needs for this in *nix-land than Windows, but it definitely does come up (I built the last one as part of a SLES distro-upgrade play). 

@nitzmahone Interesting that you already have done this. The delay could be self-imposed by the action-plugin if the command would not support this (or if it is less than 1min). A long delay is useful to warn users. A notification would be optional (and may not be supported by the command). It could be implemented on top and executed by the action plugin. (i.e. powershell stuff on Windows).

So nothing we cannot overcome, but indeed it will have lots of platform-specific tweaks to make it behave as we want.

There's some discussion about current & possible approaches to rebooting linux nodes here, along with working examples: #10616

This! Would be life-changing for things like selinux changes.
I have written a hacked up linux version myself a few times. Latest example:
https://github.com/jctanner/vmworld-demos/blob/master/library/reboot.py
https://github.com/jctanner/vmworld-demos/blob/master/action_plugins/reboot.py

I agree that the project and it's users need this as a feature. I would prefer that community builds it and collaborates.
I will take this up and propose a module with a generic structure. We can then discuss functionality, implementation and exceptions.
@dagwieers thanks for starting working on this.

I get that this is still WIP, but i wanted to suggest that 
```
   # FUTURE: ensure that a reboot has actually occurred by watching for change in last boot time fact
really needs to be a FIXME not a FUTURE
```

If this check isn't there, then the tests that check that 'the box is back up'  (transport_test and run_test_command or ping_module_test) might actually run before the machine has gone down completely.   In such a case, the tests all pass, the module returns, the playbook continues, and _then_ the machine actually goes down, possibly screwing up the rest of the playbook.


@gavin-romig-koch That comment is taken from win_reboot, and I agree with you. But the reason it's not there is that it is not simple to do (because very OS-specific).
One of my early prototypes of reboot/win_reboot actually used the setup module to gather the last boot time fact before and after the reboot. It's still not ideal, as it's somewhat heavyweight/wasteful, and we're not guaranteed that every platform will be able to provide that fact properly (eg, MacOS and FreeBSD IIRC), but at least it moves the maintenance burden away from this action. We could still also use the current behavior as an automatic fallback if the fact isn't provided.
@nitzmahone True, but I always considered it optional to run setup. And this would make it mandatory :-/ But I can live with that.
There have been a couple of real-world issues with the current win_reboot impl that would be solved by that change. The biggest one is for things like Virtualbox/VMWare that use port forwarding. Apparently the virtual network impl continues to answer on the forwarded port, which breaks the "wait for the port to go down" behavior. Then of course, there's always the shutdown race, where it's possible we miss it. That seems much more likely on, say, a container or a hypothetical future version of Windows that boots hella fast, but the likelihood of hitting it increases as things get faster.
And just to be clear, I'm talking about running setup from within this action, not externally- I don't want to force someone to have run gather_facts ahead of time. But, like I said, it's wasteful if we've already done it and the fact is correct.
@dagwieers, i've no argument with "not simple to do".

I wonder if, instead of using a fact, it would be better to have another OS-specific cmd that returns uptime.  The "shutdown" cmd itself is very OS-specific, and it seems like resolving the two OS-specific problems in the same way is gooderer.

On a related, but more ambitious tack, would it be better to wrap the OS-specific stuff (shutdown and uptime and stability) into an OS-specific module, like is done for "package" and (rpm, deb, ...).  Instead of it's behavior being unconditional, it would be based on the current state of the machine.   Instead of just "reboot", it would be "rebooted since <date/time>".   This OS-specific module could then do whatever was needed to on that OS to check, is it already going down because of a previous cmd, has it been rebooted since the the given date, is it stable, ....  And it could return what ever facts seemed useful.  The action_plugin, if not given a date, would default to the current date/time, and begin polling with that date, and continue until success, error, or timeout.

Why do we need to do an uptime check?  Just as a sanity "we rebooted" thing?  In my personal implementation, what I end up doing is 5-6 steps.  (Some of which are specific to redhat 5/6)
1) issue reboot
2) wait_for ssh to stop - with timeout
3) I think I wait for the machine to become unreachable here, don't recall
4) wait_for ssh to start - with timeout
5) wait_for /var/whatever/subsys/local to exist - signifies S99local ran - with timeout
6) Ghastly ps check to make sure all the rc scripts are done - with timeout (because some people like adding a pile of extra S99 scripts in)

I need to know the machine is now operational.  Meaning all services and databases and whatnot have resumed.  The abomination I was required to write to achieve this is highly non-ideal.  (I call ansible-playbook as a local action with a reboot.yml play)  This was required, as some machines take 20 minutes to reboot, with the delays spread across different steps.  IE: Machine A takes 5 minutes to shut down, but machine B takes 5 minutes to start.  If these are individual steps, then the total time is 10 minutes, as opposed to a more realistic 6 minutes or so for both.

I'm not sure how to do this as a module, as it involves bouncing between the module pinging the remote machine, and then swapping to running commands on it.
garbled1, this is exactly the situation I run into with reboots in a large environment (1000+) Linux servers with some VM's and some physicals. As you know most of the VM's drop fast but the physical servers can sometimes take 20 minutes just to go down. If you add a big delay you slow down the entire process, if you don't ansible returns a false positive because it checks for connection before the server has gone down. 
[bump] Regarding shutdown functionality: there's certainly a use for that, e.g. cleanly shutting down a VM or instance in your favourite cloud, before imaging it, moving it, or whatever. You can't always be sure that a 'stop' command in the hypervisor management/cloud API initiates a proper shutdown. The specific drivers for the hypervisor might not have been installed properly, or they just have been installed but need a reboot to be functional.
Although the machine that executes the shutdown is no longer reachable, the next step in the playbook would probably be a local_action that does things with the hypervisor.
Is this planned for 2.7?

Resolving patch:
diff --git a/lib/ansible/modules/system/reboot.py b/lib/ansible/modules/system/reboot.py
new file mode 100644
--- /dev/null
+++ b/lib/ansible/modules/system/reboot.py
@@ -0,0 +1,81 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+                    'status': ['preview'],
+                    'supported_by': 'core'}
+
+DOCUMENTATION = r'''
+module: reboot
+short_description: Reboot a machine
+description:
+     - Reboot a machine, wait for it to go down, come back up, and respond to commands.
+version_added: "2.7"
+options:
+  pre_reboot_delay:
+    description:
+      - Seconds for shutdown to wait before requesting reboot.
+      - On Linux and macOS, this is converted to minutes and rounded down. If less than 60, it will be set to 0.
+      - On Solaris and FreeBSD, this will be seconds.
+    default: 0
+    type: int
+  post_reboot_delay:
+    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
+    type: int
+  reboot_timeout:
+    description:
+      - Maximum seconds to wait for machine to reboot and respond to a test command.
+      - This timeout is evaluated separately for both network connection and test command success so the
+        maximum execution time for the module is twice this amount.
+    default: 600
+    type: int
+  connect_timeout:
+    description:
+      - Maximum seconds to wait for a successful connection to the managed hosts before trying again.
+      - If unspecified, the default setting for the underlying connection plugin is used.
+    type: int
+  test_command:
+    description:
+      - Command to run on the rebooted host and expect success from to determine the machine is ready for
+        further tasks.
+    default: whoami
+    type: str
+  msg:
+    description:
+      - Message to display to users before reboot.
+    default: Reboot initiated by Ansible
+    type: str
+author:
+    - Matt Davis (@nitzmahone)
+    - Sam Doran (@samdoran)
+'''
+
+EXAMPLES = r'''
+# Unconditionally reboot the machine with all defaults
+- reboot:
+
+# Reboot a slow machine that might have lots of updates to apply
+- reboot:
+    reboot_timeout: 3600
+'''
+
+RETURN = r'''
+rebooted:
+  description: true if the machine was rebooted
+  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/reboot.py b/lib/ansible/plugins/action/reboot.py
new file mode 100644
--- /dev/null
+++ b/lib/ansible/plugins/action/reboot.py
@@ -0,0 +1,287 @@
+# (c) 2016-2018, Matt Davis <mdavis@ansible.com>
+# (c) 2018, Sam Doran <sdoran@redhat.com>
+# 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
+
+import random
+import time
+
+from datetime import datetime, timedelta
+
+from ansible.errors import AnsibleError
+from ansible.plugins.action import ActionBase
+from ansible.module_utils._text import to_native, to_text
+
+
+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_REBOOT_TIMEOUT = 600
+    DEFAULT_CONNECT_TIMEOUT = None
+    DEFAULT_PRE_REBOOT_DELAY = 0
+    DEFAULT_POST_REBOOT_DELAY = 0
+    DEFAULT_TEST_COMMAND = 'whoami'
+    DEFAULT_BOOT_TIME_COMMAND = 'who -b'
+    DEFAULT_REBOOT_MESSAGE = 'Reboot initiated by Ansible'
+    DEFAULT_SHUTDOWN_COMMAND = 'shutdown'
+    DEFAULT_SUDOABLE = True
+
+    DEPRECATED_ARGS = {}
+
+    SHUTDOWN_COMMANDS = {
+        'linux': DEFAULT_SHUTDOWN_COMMAND,
+        'freebsd': DEFAULT_SHUTDOWN_COMMAND,
+        'sunos': '/usr/sbin/shutdown',
+        'darwin': '/sbin/shutdown',
+    }
+
+    SHUTDOWN_COMMAND_ARGS = {
+        'linux': '-r {delay_min} "{message}"',
+        'freebsd': '-r +{delay_sec}s "{message}"',
+        'sunos': '-y -g {delay_sec} -r "{message}"',
+        'darwin': '-r +{delay_min_macos} "{message}"'
+    }
+
+    def __init__(self, *args, **kwargs):
+        super(ActionModule, self).__init__(*args, **kwargs)
+
+        self._original_connection_timeout = None
+        self._previous_boot_time = None
+
+    def deprecated_args(self):
+        for arg, version in self.DEPRECATED_ARGS.items():
+            if self._task.args.get(arg) is not None:
+                display.warning("Since Ansible %s, %s is no longer a valid option for %s" % (version, arg, self._task.action))
+
+    def construct_command(self):
+        # Determine the system distribution in order to use the correct shutdown command arguments
+        uname_result = self._low_level_execute_command('uname')
+        distribution = uname_result['stdout'].strip().lower()
+
+        shutdown_command = self.SHUTDOWN_COMMANDS.get(distribution, self.SHUTDOWN_COMMAND_ARGS['linux'])
+        shutdown_command_args = self.SHUTDOWN_COMMAND_ARGS.get(distribution, self.SHUTDOWN_COMMAND_ARGS['linux'])
+
+        pre_reboot_delay = int(self._task.args.get('pre_reboot_delay', self.DEFAULT_PRE_REBOOT_DELAY))
+        if pre_reboot_delay < 0:
+            pre_reboot_delay = 0
+
+        # Convert seconds to minutes for Linux. If less that 60, set it to 0 except for macOS which will
+        # sever the connection too quickly if set to 0, so set that to 1.
+        # We could simplify this by setting them both to 1, but I think of all the time that
+        # people will lose waiting for that extra 1 minute delay and want to give them their
+        # lives back.
+        delay_min = pre_reboot_delay // 60
+        delay_min_macos = delay_min | 1
+        msg = self._task.args.get('msg', self.DEFAULT_REBOOT_MESSAGE)
+
+        shutdown_command_args = shutdown_command_args.format(delay_sec=pre_reboot_delay, delay_min=delay_min, delay_min_macos=delay_min_macos, message=msg)
+
+        reboot_command = '%s %s' % (shutdown_command, shutdown_command_args)
+        return reboot_command
+
+    def get_system_boot_time(self):
+        command_result = self._low_level_execute_command(self.DEFAULT_BOOT_TIME_COMMAND, sudoable=self.DEFAULT_SUDOABLE)
+
+        # For single board computers, e.g., Raspberry Pi, that lack a real time clock and are using fake-hwclock
+        # launched by systemd, the update of utmp/wtmp is not done correctly.
+        # Fall back to using uptime -s for those systems.
+        # https://github.com/systemd/systemd/issues/6057
+        if '1970-01-01 00:00' in command_result['stdout']:
+            command_result = self._low_level_execute_command('uptime -s', sudoable=self.DEFAULT_SUDOABLE)
+
+        if command_result['rc'] != 0:
+            raise AnsibleError("%s: failed to get host boot time info, rc: %d, stdout: %s, stderr: %s"
+                               % (self._task.action, command_result.rc, to_native(command_result['stdout']), to_native(command_result['stderr'])))
+
+        return command_result['stdout'].strip()
+
+    def check_boot_time(self):
+        display.vvv("%s: attempting to get system boot time" % self._task.action)
+        connect_timeout = self._task.args.get('connect_timeout', self._task.args.get('connect_timeout_sec', self.DEFAULT_CONNECT_TIMEOUT))
+
+        # override connection timeout from defaults to custom value
+        if connect_timeout:
+            try:
+                self._connection.set_option("connection_timeout", connect_timeout)
+                self._connection.reset()
+            except AttributeError:
+                display.warning("Connection plugin does not allow the connection timeout to be overridden")
+
+        # try and get boot time
+        try:
+            current_boot_time = self.get_system_boot_time()
+        except Exception as e:
+            raise e
+
+        # FreeBSD returns an empty string immediately before reboot so adding a length
+        # check to prevent prematurely assuming system has rebooted
+        if len(current_boot_time) == 0 or current_boot_time == self._previous_boot_time:
+            raise Exception("boot time has not changed")
+
+    def run_test_command(self, **kwargs):
+        test_command = self._task.args.get('test_command', self.DEFAULT_TEST_COMMAND)
+        display.vvv("%s: attempting post-reboot test command '%s'" % (self._task.action, test_command))
+        command_result = self._low_level_execute_command(test_command, sudoable=self.DEFAULT_SUDOABLE)
+
+        result = {}
+        if command_result['rc'] != 0:
+            result['failed'] = True
+            result['msg'] = 'test command failed: %s %s' % (to_native(command_result['stderr'], to_native(command_result['stdout'])))
+        else:
+            result['msg'] = to_native(command_result['stdout'])
+
+        return result
+
+    def do_until_success_or_timeout(self, action, reboot_timeout, action_desc):
+        max_end_time = datetime.utcnow() + timedelta(seconds=reboot_timeout)
+
+        fail_count = 0
+        max_fail_sleep = 12
+
+        while datetime.utcnow() < max_end_time:
+            try:
+                action()
+                if action_desc:
+                    display.debug('%s: %s success' % (self._task.action, action_desc))
+                return
+            except Exception as e:
+                # Use exponential backoff with a max timout, plus a little bit of randomness
+                random_int = random.randint(0, 1000) / 1000
+                fail_sleep = 2 ** fail_count + random_int
+                if fail_sleep > max_fail_sleep:
+
+                    fail_sleep = max_fail_sleep + random_int
+                if action_desc:
+                    display.debug("{0}: {1} fail '{2}', retrying in {3:.4} seconds...".format(self._task.action, action_desc, to_text(e), fail_sleep))
+                fail_count += 1
+                time.sleep(fail_sleep)
+
+        raise TimedOutException('Timed out waiting for %s' % (action_desc))
+
+    def perform_reboot(self):
+        display.debug("%s: rebooting server" % self._task.action)
+
+        remote_command = self.construct_command()
+        reboot_result = self._low_level_execute_command(remote_command, sudoable=self.DEFAULT_SUDOABLE)
+        result = {}
+        result['start'] = datetime.utcnow()
+
+        if reboot_result['rc'] != 0:
+            result['failed'] = True
+            result['rebooted'] = False
+            result['msg'] = "Shutdown command failed. Error was %s, %s" % (
+                to_native(reboot_result['stdout'].strip()), to_native(reboot_result['stderr'].strip()))
+            return result
+
+        result['failed'] = False
+
+        # attempt to store the original connection_timeout option var so it can be reset after
+        self._original_connection_timeout = None
+        try:
+            self._original_connection_timeout = self._connection.get_option('connection_timeout')
+        except AnsibleError:
+            display.debug("%s: connect_timeout connection option has not been set" % self._task.action)
+
+        post_reboot_delay = int(self._task.args.get('post_reboot_delay', self._task.args.get('post_reboot_delay_sec', self.DEFAULT_POST_REBOOT_DELAY)))
+        if post_reboot_delay < 0:
+            post_reboot_delay = 0
+
+        if post_reboot_delay != 0:
+            display.vvv("%s: waiting an additional %d seconds" % (self._task.action, post_reboot_delay))
+            time.sleep(post_reboot_delay)
+
+        return result
+
+    def validate_reboot(self):
+        display.debug('%s: Validating reboot' % self._task.action)
+        result = {}
+
+        try:
+            # keep on checking system boot_time with short connection responses
+            reboot_timeout = int(self._task.args.get('reboot_timeout', self._task.args.get('reboot_timeout_sec', self.DEFAULT_REBOOT_TIMEOUT)))
+            connect_timeout = self._task.args.get('connect_timeout', self._task.args.get('connect_timeout_sec', self.DEFAULT_CONNECT_TIMEOUT))
+            self.do_until_success_or_timeout(self.check_boot_time, reboot_timeout, action_desc="boot_time check")
+
+            if connect_timeout:
+                # reset the connection to clear the custom connection timeout
+                try:
+                    self._connection.set_option("connection_timeout", connect_timeout)
+                    self._connection.reset()
+                except (AnsibleError, AttributeError) as e:
+                    display.debug("Failed to reset connection_timeout back to default: %s" % to_text(e))
+
+            # finally run test command to ensure everything is working
+            # 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(self.run_test_command, reboot_timeout, action_desc="post-reboot test command")
+
+            result['rebooted'] = True
+            result['changed'] = True
+
+        except TimedOutException as toex:
+            result['failed'] = True
+            result['rebooted'] = True
+            result['msg'] = to_text(toex)
+            return result
+
+        return result
+
+    def run(self, tmp=None, task_vars=None):
+        self._supports_check_mode = True
+        self._supports_async = True
+
+        # If running with local connection, fail so we don't reboot ourself
+        if self._connection.transport == 'local':
+            msg = 'Running {0} with local connection would reboot the control node.'.format(self._task.action)
+            return dict(changed=False, elapsed=0, rebooted=False, failed=True, msg=msg)
+
+        if self._play_context.check_mode:
+            return dict(changed=True, elapsed=0, rebooted=True)
+
+        if task_vars is None:
+            task_vars = dict()
+
+        self.deprecated_args()
+
+        result = super(ActionModule, self).run(tmp, task_vars)
+
+        if result.get('skipped', False) or result.get('failed', False):
+            return result
+
+        # Get current boot time
+        try:
+            self._previous_boot_time = self.get_system_boot_time()
+        except Exception as e:
+            result['failed'] = True
+            result['reboot'] = False
+            result['msg'] = to_text(e)
+            return result
+
+        # Initiate reboot
+        reboot_result = self.perform_reboot()
+
+        if reboot_result['failed']:
+            result = reboot_result
+            elapsed = datetime.utcnow() - reboot_result['start']
+            result['elapsed'] = elapsed.seconds
+            return result
+
+        # Make sure reboot was successful
+        result = self.validate_reboot()
+
+        elapsed = datetime.utcnow() - reboot_result['start']
+        result['elapsed'] = elapsed.seconds
+
+        return result
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,16 +1,15 @@
-# (c) 2016, Matt Davis <mdavis@ansible.com>
+# (c) 2018, Matt Davis <mdavis@ansible.com>
 # 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
 
-import time
-
-from datetime import datetime, timedelta
+from datetime import datetime
 
 from ansible.errors import AnsibleError
 from ansible.plugins.action import ActionBase
 from ansible.module_utils._text import to_native
+from ansible.plugins.action.reboot import ActionModule as RebootActionModule
 
 try:
     from __main__ import display
@@ -23,201 +22,66 @@ class TimedOutException(Exception):
     pass
 
 
-class ActionModule(ActionBase):
+class ActionModule(RebootActionModule, ActionBase):
     TRANSFERS_FILES = False
 
-    DEFAULT_REBOOT_TIMEOUT = 600
     DEFAULT_CONNECT_TIMEOUT = 5
     DEFAULT_PRE_REBOOT_DELAY = 2
-    DEFAULT_POST_REBOOT_DELAY = 0
-    DEFAULT_TEST_COMMAND = 'whoami'
-    DEFAULT_REBOOT_MESSAGE = 'Reboot initiated by Ansible.'
-
-    def get_system_uptime(self):
-        uptime_command = "(Get-WmiObject -ClassName Win32_OperatingSystem).LastBootUpTime"
-        (rc, stdout, stderr) = self._connection.exec_command(uptime_command)
-
-        if rc != 0:
-            raise Exception("win_reboot: failed to get host uptime info, rc: %d, stdout: %s, stderr: %s"
-                            % (rc, stdout, stderr))
-
-        return stdout
-
-    def do_until_success_or_timeout(self, what, timeout, what_desc, fail_sleep=1):
-        max_end_time = datetime.utcnow() + timedelta(seconds=timeout)
+    DEFAULT_BOOT_TIME_COMMAND = "(Get-WmiObject -ClassName Win32_OperatingSystem).LastBootUpTime"
+    DEFAULT_SHUTDOWN_COMMAND_ARGS = '/r /t %d /c "%s"'
+    DEFAULT_SUDOABLE = False
 
-        exc = ""
-        while datetime.utcnow() < max_end_time:
-            try:
-                what()
-                if what_desc:
-                    display.debug("win_reboot: %s success" % what_desc)
-                return
-            except Exception as e:
-                exc = e
-                if what_desc:
-                    display.debug("win_reboot: %s fail '%s' (expected), retrying in %d seconds..." % (what_desc, to_native(e), fail_sleep))
-                time.sleep(fail_sleep)
+    DEPRECATED_ARGS = {
+        'shutdown_timeout': '2.5',
+        'shutdown_timeout_sec': '2.5',
+    }
 
-        raise TimedOutException("timed out waiting for %s: %s" % (what_desc, exc))
+    def construct_command(self):
+        shutdown_command = self.DEFAULT_SHUTDOWN_COMMAND
+        pre_reboot_delay = int(self._task.args.get('pre_reboot_delay', self._task.args.get('pre_reboot_delay_sec', self.DEFAULT_PRE_REBOOT_DELAY)))
+        msg = self._task.args.get('msg', self.DEFAULT_REBOOT_MESSAGE)
+        shutdown_command_args = self.DEFAULT_SHUTDOWN_COMMAND_ARGS % (pre_reboot_delay, msg)
 
-    def run(self, tmp=None, task_vars=None):
+        reboot_command = '%s %s' % (shutdown_command, shutdown_command_args)
+        return reboot_command
 
-        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)
-        del tmp  # tmp no longer has any effect
-
-        if result.get('skipped', False) or result.get('failed', False):
-            return result
+    def perform_reboot(self):
+        display.debug("Rebooting server")
 
-        # Handle timeout parameters and its alias
-        deprecated_args = {
-            'shutdown_timeout': '2.5',
-            'shutdown_timeout_sec': '2.5',
-        }
-        for arg, version in deprecated_args.items():
-            if self._task.args.get(arg) is not None:
-                display.warning("Since Ansible %s, %s is no longer used with win_reboot" % (version, arg))
-
-        if self._task.args.get('connect_timeout') is not None:
-            connect_timeout = int(self._task.args.get('connect_timeout', self.DEFAULT_CONNECT_TIMEOUT))
-        else:
-            connect_timeout = int(self._task.args.get('connect_timeout_sec', self.DEFAULT_CONNECT_TIMEOUT))
-
-        if self._task.args.get('reboot_timeout') is not None:
-            reboot_timeout = int(self._task.args.get('reboot_timeout', self.DEFAULT_REBOOT_TIMEOUT))
-        else:
-            reboot_timeout = int(self._task.args.get('reboot_timeout_sec', self.DEFAULT_REBOOT_TIMEOUT))
-
-        if self._task.args.get('pre_reboot_delay') is not None:
-            pre_reboot_delay = int(self._task.args.get('pre_reboot_delay', self.DEFAULT_PRE_REBOOT_DELAY))
-        else:
-            pre_reboot_delay = int(self._task.args.get('pre_reboot_delay_sec', self.DEFAULT_PRE_REBOOT_DELAY))
-
-        if self._task.args.get('post_reboot_delay') is not None:
-            post_reboot_delay = int(self._task.args.get('post_reboot_delay', self.DEFAULT_POST_REBOOT_DELAY))
-        else:
-            post_reboot_delay = int(self._task.args.get('post_reboot_delay_sec', self.DEFAULT_POST_REBOOT_DELAY))
-
-        test_command = str(self._task.args.get('test_command', self.DEFAULT_TEST_COMMAND))
-        msg = str(self._task.args.get('msg', self.DEFAULT_REBOOT_MESSAGE))
-
-        # Get current uptime
-        try:
-            before_uptime = self.get_system_uptime()
-        except Exception as e:
-            result['failed'] = True
-            result['reboot'] = False
-            result['msg'] = to_native(e)
-            return result
+        remote_command = self.construct_command()
+        reboot_result = self._low_level_execute_command(remote_command, sudoable=self.DEFAULT_SUDOABLE)
 
-        # Initiate reboot
-        display.vvv("rebooting server")
-        (rc, stdout, stderr) = self._connection.exec_command('shutdown /r /t %d /c "%s"' % (pre_reboot_delay, msg))
+        pre_reboot_delay = int(self._task.args.get('pre_reboot_delay', self._task.args.get('pre_reboot_delay_sec', self.DEFAULT_PRE_REBOOT_DELAY)))
 
         # Test for "A system shutdown has already been scheduled. (1190)" and handle it gracefully
-        if rc == 1190 or (rc != 0 and b"(1190)" in stderr):
+        if reboot_result['rc'] == 1190 or (reboot_result['rc'] != 0 and b"(1190)" in reboot_result['stderr']):
             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')
+            result1 = self._low_level_execute_command('shutdown /a', sudoable=self.DEFAULT_SUDOABLE)
+            # (reboot_result['rc'], stdout1, stderr1) = self._connection.exec_command('shutdown /a')
 
             # Initiate reboot again
-            (rc, stdout2, stderr2) = self._connection.exec_command('shutdown /r /t %d' % pre_reboot_delay)
-            stdout += stdout1 + stdout2
-            stderr += stderr1 + stderr2
+            result2 = self._connection.exec_command('shutdown /r /t %d' % pre_reboot_delay)
+            # (reboot_result['rc'], stdout2, stderr2) = self._connection.exec_command('shutdown /r /t %d' % pre_reboot_delay)
 
-        if rc != 0:
+            stdout = reboot_result['stdout'] + result1['stdout'] + result2['stdout']
+            stderr = reboot_result['stderr'] + result1['stderr'] + result2['stderr']
+
+        result = {}
+        if reboot_result['rc'] != 0:
             result['failed'] = True
             result['rebooted'] = False
-            result['msg'] = "Shutdown command failed, error text was '%s'" % to_native(stderr)
+            result['msg'] = "Shutdown command failed, error was: %s %s" % (to_native(stdout.strip()), to_native('stderr'.strip()))
             return result
 
-        start = datetime.now()
+        result['failed'] = False
+        result['start'] = datetime.utcnow()
+
         # Get the original connection_timeout option var so it can be reset after
-        connection_timeout_orig = None
         try:
-            connection_timeout_orig = self._connection.get_option('connection_timeout')
+            self._original_connection_timeout = self._connection.get_option('connection_timeout')
         except AnsibleError:
-            display.debug("win_reboot: connection_timeout connection option has not been set")
-
-        try:
-            # keep on checking system uptime with short connection responses
-            def check_uptime():
-                display.vvv("attempting to get system uptime")
-
-                # override connection timeout from defaults to custom value
-                try:
-                    self._connection.set_option("connection_timeout",
-                                                connect_timeout)
-                    self._connection.reset()
-                except AttributeError:
-                    display.warning("Connection plugin does not allow the "
-                                    "connection timeout to be overridden")
-
-                # try and get uptime
-                try:
-                    current_uptime = self.get_system_uptime()
-                except Exception as e:
-                    raise e
-
-                if current_uptime == before_uptime:
-                    raise Exception("uptime has not changed")
-
-            self.do_until_success_or_timeout(check_uptime, reboot_timeout, what_desc="reboot uptime check success")
-
-            # reset the connection to clear the custom connection timeout
-            try:
-                self._connection.set_option("connection_timeout",
-                                            connection_timeout_orig)
-                self._connection.reset()
-            except (AnsibleError, AttributeError) as e:
-                display.debug("Failed to reset connection_timeout back to default: %s" % to_native(e))
-
-            # finally run test command to ensure everything is working
-            def run_test_command():
-                display.vvv("attempting post-reboot test command '%s'" % test_command)
-                try:
-                    (rc, stdout, stderr) = self._connection.exec_command(test_command)
-                except Exception as e:
-                    # in case of a failure trying to execute the command
-                    # (another reboot occurred) we need to reset the connection
-                    # to make sure we are not re-using the same shell id
-                    try:
-                        self._connection.reset()
-                    except AttributeError:
-                        pass
-                    raise
-                else:
-                    if rc != 0:
-                        raise Exception("test command failed, stdout: '%s', stderr: '%s', rc: %d"
-                                        % (stdout, stderr, rc))
-
-            # 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, 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'] = to_native(toex)
-
-        if post_reboot_delay != 0:
-            display.vvv("win_reboot: waiting an additional %d seconds" % post_reboot_delay)
-            time.sleep(post_reboot_delay)
-
-        elapsed = datetime.now() - start
-        result['elapsed'] = elapsed.seconds
+            display.debug("%s: connect_timeout connection option has not been set" % self._task.action)
 
         return result


Test patch:
diff --git a/test/integration/targets/reboot/aliases b/test/integration/targets/reboot/aliases
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/reboot/aliases
@@ -0,0 +1,2 @@
+# No current way to split controller and test node
+unsupported
diff --git a/test/integration/targets/reboot/tasks/main.yml b/test/integration/targets/reboot/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/reboot/tasks/main.yml
@@ -0,0 +1,50 @@
+- block:
+    # This block can be removed once we have a mechanism in ansible-test to separate
+    # the control node from the managed node.
+    - block:
+        - name: Write temp file for sanity checking this is not the controller
+          copy:
+            content: 'I am the control node'
+            dest: /tmp/Anything-Nutlike-Nuzzle-Plow-Overdue
+          delegate_to: localhost
+
+        - name: See if the temp file exists on the managed node
+          stat:
+            path: /tmp/Anything-Nutlike-Nuzzle-Plow-Overdue
+          register: controller_temp_file
+
+        - name: EXPECT FAILURE | Check if the managed node is the control node
+          assert:
+            msg: >
+              This test must be run manually by modifying the inventory file to point
+              "{{ inventory_hostname }}" at a remote host rather than "{{ ansible_host }}".
+              Skipping reboot test.
+            that:
+              - not controller_temp_file.stat.exists
+
+    - name: Get current boot time
+      command: who -b
+      register: before_boot_time
+
+    - name: Reboot with default settings
+      reboot:
+      register: reboot_result
+
+    - name: Get current boot time
+      command: who -b
+      register: after_boot_time
+
+    - name: Enusure system was actually rebooted
+      assert:
+        that:
+          - reboot_result is changed
+          - reboot_result.elapsed > 10
+          - before_boot_time.stdout != after_boot_time.stdout
+
+  always:
+    - name: Cleanup temp file
+      file:
+        path: /tmp/Anything-Nutlike-Nuzzle-Plow-Overdue
+        state: absent
+
+  when: ansible_virtualization_type | default('') != 'docker'