Repository: ansible/ansible

Issue:
pause module no longer allows abort
##### ISSUE TYPE

 - Bug Report

##### COMPONENT NAME

pause

##### ANSIBLE VERSION
`ansible --version`:

```
ansible 2.5.0 (devel 90f0c411e4) last updated 2018/01/20 04:59:06 (GMT +000)
  config file = /Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg
  configured module search path = [u'/Users/jcuzella/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /opt/ansible/ansible/lib/ansible
  executable location = /opt/ansible/ansible/bin/ansible
  python version = 2.7.5 (default, Aug  4 2017, 00:39:18) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)]
```

##### CONFIGURATION

Results of `ansible-config dump --only-changed`:

```
[DEPRECATION WARNING]: DEFAULT_SUDO_FLAGS option, In favor of become which is a generic framework , use become instead. This feature will be removed in version 2.8. Deprecation warnings can be disabled by setting
deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: ANSIBLE_HOSTS option, The variable is misleading as it can be a list of hosts and/or paths to inventory sources , use ANSIBLE_INVENTORY instead. This feature will be removed in version 2.8. Deprecation warnings
can be disabled by setting deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: DEFAULT_SUDO_EXE option, In favor of become which is a generic framework , use become instead. This feature will be removed in version 2.8. Deprecation warnings can be disabled by setting
deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: DEFAULT_SUDO_USER option, In favor of become which is a generic framework , use become instead. This feature will be removed in version 2.8. Deprecation warnings can be disabled by setting
deprecation_warnings=False in ansible.cfg.
ANSIBLE_SSH_ARGS(env: ANSIBLE_SSH_ARGS) =  -o ForwardAgent=yes -o ControlPath=/tmp/ssh-%r@%h:%p -o ControlMaster=auto -o ControlPersist=60s
DEFAULT_ACTION_PLUGIN_PATH(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = [u'/usr/share/ansible_plugins/action_plugins']
DEFAULT_CALLBACK_PLUGIN_PATH(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = [u'/usr/share/ansible_plugins/callback_plugins']
DEFAULT_CONNECTION_PLUGIN_PATH(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = [u'/usr/share/ansible_plugins/connection_plugins']
DEFAULT_FILTER_PLUGIN_PATH(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = [u'/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/filter_plugins']
DEFAULT_FORKS(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = 5
DEFAULT_HOST_LIST(env: ANSIBLE_HOSTS) = [u'/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/inventory']
DEFAULT_LOOKUP_PLUGIN_PATH(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = [u'/usr/share/ansible_plugins/lookup_plugins']
DEFAULT_MANAGED_STR(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = This file is managed by Ansible, all changes will be lost.
DEFAULT_POLL_INTERVAL(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = 15
DEFAULT_REMOTE_PORT(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = 22
DEFAULT_ROLES_PATH(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = [u'/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/roles']
DEFAULT_SUDO_EXE(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = sudo
DEFAULT_SUDO_FLAGS(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = -H
DEFAULT_SUDO_USER(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = root
DEFAULT_TIMEOUT(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = 60
DEFAULT_TRANSPORT(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = smart
DEFAULT_VARS_PLUGIN_PATH(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = [u'/usr/share/ansible_plugins/vars_plugins']
HOST_KEY_CHECKING(/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg) = False
```

##### OS / ENVIRONMENT

ansible running from CentOS 7 docker container (Reproduction steps & [test case repo][ansible-pause-bug-reproduction] provided!)

##### SUMMARY

As documented in the [`pause` module documentation][1], the module should accept a `Ctrl+c` input followed by either character: `a`, or `c` to abort or continue the playbook respectively:

> You can use `ctrl+c` if you wish to advance a pause earlier than it is set to expire or if you need to abort a playbook run entirely. To continue early press `ctrl+c` and then `c`. To abort a playbook press `ctrl+c` and then `a`.

Currently Ansible's `pause` module does not appear to accept `Ctrl+c` anymore, instead only pausing indefinitely (or until user forcibly kills the `ansible` process via another shell command), or accepting `Enter` to continue.  This is probably a behavior regression.  Only other related issue was #11386, but this was supposedly solved?

##### STEPS TO REPRODUCE

```

mkdir -p ~/src/pub/
cd ~/src/pub/
git clone git@github.com:trinitronx/ansible-pause-bug-reproduction.git
cd ansible-pause-bug-reproduction/
# Note: docker-run script assumes it is running somewhere under your $HOME dir!  This is for volume mounting playbook directory into docker container with the version of Ansible we can reproduce this on
./bin/docker-run 'ansible-playbook -i inventory/hosts playbooks/pause-test/pause-test.yml  -vvv --diff'
```

```yaml
---
# Bug reproduction to test Ctrl-c behavior on pause module
- name: Test pause module
  hosts: "localhost"
  become: no
  gather_facts: no
  tasks:
    - pause:
      prompt: "Uh-oh! Something doesn't look right... Press Ctlr-c to try and abort and hope for the best?"
      tags:
        - test_pause

    - debug: msg="I'm sorry Dave, I can't do that"
      tags:
        - test_pause
        - debug

    - name: We should never get here if you pressed Ctrl-c and aborted...
      debug: msg="Ooops, we didn't abort... now the playbook has done bad things even though you tried to stop it!"
      tags:
        - test_pause
        - debug

    - name: We should never get here if you pressed Ctrl-c and aborted...
      fail:
      tags:
        - test_pause
```

##### EXPECTED RESULTS

Ansible should have caught `Ctrl+c` input, and output the message:

`"Press 'C' to continue the play or 'A' to abort"`

Inputting `a` should have aborted the playbook run, and subsequent tasks should have never run.

##### ACTUAL RESULTS

Pause module never appears to catch `Ctrl+c` input and output the `"Press 'C' to continue the play or 'A' to abort"` message like it used to in older versions!  This could result in potentially bad behavior or consequences when playbook developers put in a `pause` task in order to have human intervention at some point in a playbook.  For example: to check some registered variables or state before continuing on with the rest of the playbook tasks.

If the user presses `Enter`, and is running with `-vvv` output, then the input characters are shown back to the user in `user_input`.  Clearly Ansible sees the input characters, but no longer acts on them.

- `Ctrl+c` is reported as `\u0003`, which is ["End of Text"][2]
- `Escape` is reported as `\u001b`, which is ["Escape"][3].

**Example playbook output**:
```
INFO: Found /tmp/ssh-C8ukortCdm/agent.23383
INFO: Running ssh forwarding pid: 15176
rm: cannot remove '/opt/ansible/ansible/lib/ansible.egg-info': Permission denied
no previously-included directories found matching 'ticket_stubs'
no previously-included directories found matching 'hacking'
warning: no files found matching 'SYMLINK_CACHE.json'
rm: cannot remove '/opt/ansible/ansible/lib/ansible.egg-info': Permission denied
running egg_info
writing requirements to lib/ansible.egg-info/requires.txt
writing lib/ansible.egg-info/PKG-INFO
writing top-level names to lib/ansible.egg-info/top_level.txt
writing dependency_links to lib/ansible.egg-info/dependency_links.txt
writing manifest file 'lib/ansible.egg-info/SOURCES.txt'
reading manifest file 'lib/ansible.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
no previously-included directories found matching 'ticket_stubs'
no previously-included directories found matching 'hacking'
warning: no files found matching 'SYMLINK_CACHE.json'
writing manifest file 'lib/ansible.egg-info/SOURCES.txt'

Setting up Ansible to run out of checkout...

PATH=/opt/ansible/ansible/bin:/opt/ansible/ansible/test/runner:/usr/local/opt/qt/bin:/Users/jcuzella/go/bin:/usr/local/bin:/opt/ansible/ansible/bin:/opt/ansible/ansible/test/runner:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin
PYTHONPATH=/opt/ansible/ansible/lib:
MANPATH=/opt/ansible/ansible/docs/man:

Remember, you may wish to specify your host file with -i

Done!

ansible-playbook 2.5.0 (devel 90f0c411e4) last updated 2018/01/20 04:59:06 (GMT +000)
  config file = /Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg
  configured module search path = [u'/Users/jcuzella/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /opt/ansible/ansible/lib/ansible
  executable location = /opt/ansible/ansible/bin/ansible-playbook
  python version = 2.7.5 (default, Aug  4 2017, 00:39:18) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)]
Using /Users/jcuzella/src/pub/ansible-pause-bug-reproduction/ansible.cfg as config file
[DEPRECATION WARNING]: DEFAULT_SUDO_FLAGS option, In favor of become which is a generic framework , use become instead. This feature will be removed in version 2.8. Deprecation warnings can be disabled by setting
deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: ANSIBLE_HOSTS option, The variable is misleading as it can be a list of hosts and/or paths to inventory sources , use ANSIBLE_INVENTORY instead. This feature will be removed in version 2.8. Deprecation warnings
can be disabled by setting deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: DEFAULT_SUDO_EXE option, In favor of become which is a generic framework , use become instead. This feature will be removed in version 2.8. Deprecation warnings can be disabled by setting
deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: DEFAULT_SUDO_USER option, In favor of become which is a generic framework , use become instead. This feature will be removed in version 2.8. Deprecation warnings can be disabled by setting
deprecation_warnings=False in ansible.cfg.
Parsed /Users/jcuzella/src/pub/ansible-pause-bug-reproduction/inventory/hosts inventory source with ini plugin
 [WARNING]: Ignoring invalid attribute: prompt


PLAYBOOK: pause-test.yml *******************************************************************************************************************************************************************************************************************
1 plays in playbooks/pause-test/pause-test.yml

PLAY [Test pause module] *******************************************************************************************************************************************************************************************************************
META: ran handlers

TASK [pause] *******************************************************************************************************************************************************************************************************************************
task path: /Users/jcuzella/src/pub/ansible-pause-bug-reproduction/playbooks/pause-test/pause-test.yml:8
[pause]
Press enter to continue:
^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^[^Caabort!
                                                                                                                                                                                             ok: [127.0.0.1] => {
    "attempts": 1,
    "changed": false,
    "delta": 9,
    "echo": true,
    "failed": false,
    "rc": 0,
    "start": "2018-01-25 17:46:40.211179",
    "stderr": "",
    "stdout": "Paused for 0.15 minutes",
    "stop": "2018-01-25 17:46:49.384308",
    "user_input": "\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u001b\u0003aabort!"
}

TASK [debug] *******************************************************************************************************************************************************************************************************************************
task path: /Users/jcuzella/src/pub/ansible-pause-bug-reproduction/playbooks/pause-test/pause-test.yml:13
ok: [127.0.0.1] => {
    "attempts": 1,
    "failed": false,
    "msg": "I'm sorry Dave, I can't do that"
}

TASK [We should never get here if you pressed Ctrl-c and aborted...] ***********************************************************************************************************************************************************************
task path: /Users/jcuzella/src/pub/ansible-pause-bug-reproduction/playbooks/pause-test/pause-test.yml:18
ok: [127.0.0.1] => {
    "attempts": 1,
    "failed": false,
    "msg": "Ooops, we didn't abort... now the playbook has done bad things even though you tried to stop it!"
}

TASK [We should never get here if you pressed Ctrl-c and aborted...] ***********************************************************************************************************************************************************************
task path: /Users/jcuzella/src/pub/ansible-pause-bug-reproduction/playbooks/pause-test/pause-test.yml:24
fatal: [127.0.0.1]: FAILED! => {
    "attempts": 1,
    "changed": false,
    "failed": true,
    "msg": "Failed as requested from task"
}
	to retry, use: --limit @/Users/jcuzella/src/pub/ansible-pause-bug-reproduction/playbooks/pause-test/pause-test.retry

PLAY RECAP *********************************************************************************************************************************************************************************************************************************
127.0.0.1                  : ok=3    changed=0    unreachable=0    failed=1
```


[1]: http://docs.ansible.com/ansible/latest/pause_module.html
[2]: https://www.compart.com/en/unicode/U+0003
[3]: https://www.compart.com/en/unicode/U+001b
[ansible-pause-bug-reproduction]: https://github.com/trinitronx/ansible-pause-bug-reproduction


Discussion:
Files identified in the description:
* [lib/ansible/modules/utilities/logic/pause.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/utilities/logic/pause.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @tbielawa
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@ansibot 
!component +lib/ansible/plugins/action/pause.py
Files identified in the description:
* [lib/ansible/modules/utilities/logic/pause.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/utilities/logic/pause.py)
* [lib/ansible/plugins/action/pause.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/action/pause.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
Just found where the actual module file is, and upon further inspection it appears that the `Ctrl+c` handling code is in `if`/`else` blocks:

- [`Ctrl+c` Prompt only shows when `seconds` is set](https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/action/pause.py#L126-L143)
- [KeyboardInterrupt only thrown when `seconds` is set and `key_pressed == b'\x03'`](https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/action/pause.py#L183-L209)

So maybe I'm remembering this behavior differently, and it was never handling `Ctrl+c` unless `seconds` is passed to the module as argument?  Could be that this behavior is unwanted for some reason, but I'd almost expect it to always handle `Ctrl+c` according to what the docs say.
Hello,

@trinitronx , I am here to tell you that you are not crazy, it used to work the way you describe it, pressing enter would continue, pressing ctrl-c and then "a" would abort, and then... stopped working.  For now I worked around it by setting seconds to something big, like 86400, and user has to press ctrl-c c to continue (instead of enter).

On the bright side, it is less error prone and less awkward  that continuing with enter and aborting with ctr + c and then a followed by enter, but I have to specify a timeout.  They even removed the option of setting "seconds" to 0 to wait forever!


I've tested a few cases with my PR, can everyone here look at their issues and confirm that my PR fixes them?

Resolving patch:
diff --git a/lib/ansible/plugins/action/pause.py b/lib/ansible/plugins/action/pause.py
--- a/lib/ansible/plugins/action/pause.py
+++ b/lib/ansible/plugins/action/pause.py
@@ -19,14 +19,16 @@
 
 import datetime
 import signal
+import sys
 import termios
 import time
 import tty
 
 from os import isatty
 from ansible.errors import AnsibleError
+from ansible.module_utils._text import to_text, to_native
+from ansible.module_utils.parsing.convert_bool import boolean
 from ansible.module_utils.six import PY3
-from ansible.module_utils._text import to_text
 from ansible.plugins.action import ActionBase
 
 try:
@@ -35,6 +37,20 @@
     from ansible.utils.display import Display
     display = Display()
 
+try:
+    import curses
+    curses.setupterm()
+    HAS_CURSES = True
+except (ImportError, curses.error):
+    HAS_CURSES = False
+
+if HAS_CURSES:
+    MOVE_TO_BOL = curses.tigetstr('cr')
+    CLEAR_TO_EOL = curses.tigetstr('el')
+else:
+    MOVE_TO_BOL = b'\r'
+    CLEAR_TO_EOL = b'\x1b[K'
+
 
 class AnsibleTimeoutExceeded(Exception):
     pass
@@ -44,6 +60,11 @@ def timeout_handler(signum, frame):
     raise AnsibleTimeoutExceeded
 
 
+def clear_line(stdout):
+    stdout.write(b'\x1b[%s' % MOVE_TO_BOL)
+    stdout.write(b'\x1b[%s' % CLEAR_TO_EOL)
+
+
 class ActionModule(ActionBase):
     ''' pauses execution for a length or time, or until input is received '''
 
@@ -81,10 +102,11 @@ def run(self, tmp=None, task_vars=None):
 
         # Should keystrokes be echoed to stdout?
         if 'echo' in self._task.args:
-            echo = self._task.args['echo']
-            if not type(echo) == bool:
+            try:
+                echo = boolean(self._task.args['echo'])
+            except TypeError as e:
                 result['failed'] = True
-                result['msg'] = "'%s' is not a valid setting for 'echo'." % self._task.args['echo']
+                result['msg'] = to_native(e)
                 return result
 
             # Add a note saying the output is hidden if echo is disabled
@@ -96,7 +118,7 @@ def run(self, tmp=None, task_vars=None):
             prompt = "[%s]\n%s%s:" % (self._task.get_name().strip(), self._task.args['prompt'], echo_prompt)
         else:
             # If no custom prompt is specified, set a default prompt
-            prompt = "[%s]\n%s%s:" % (self._task.get_name().strip(), 'Press enter to continue', echo_prompt)
+            prompt = "[%s]\n%s%s:" % (self._task.get_name().strip(), 'Press enter to continue, Ctrl+C to interrupt', echo_prompt)
 
         # Are 'minutes' or 'seconds' keys that exist in 'args'?
         if 'minutes' in self._task.args or 'seconds' in self._task.args:
@@ -149,65 +171,83 @@ def run(self, tmp=None, task_vars=None):
             try:
                 if PY3:
                     stdin = self._connection._new_stdin.buffer
+                    stdout = sys.stdout.buffer
                 else:
                     stdin = self._connection._new_stdin
+                    stdout = sys.stdout
                 fd = stdin.fileno()
             except (ValueError, AttributeError):
                 # ValueError: someone is using a closed file descriptor as stdin
                 # AttributeError: someone is using a null file descriptor as stdin on windoez
                 stdin = None
+
             if fd is not None:
                 if isatty(fd):
+
+                    # grab actual Ctrl+C sequence
+                    try:
+                        intr = termios.tcgetattr(fd)[6][termios.VINTR]
+                    except Exception:
+                        # unsupported/not present, use default
+                        intr = b'\x03'  # value for Ctrl+C
+
+                    # get backspace sequences
+                    try:
+                        backspace = termios.tcgetattr(fd)[6][termios.VERASE]
+                    except Exception:
+                        backspace = [b'\x7f', b'\x08']
+
                     old_settings = termios.tcgetattr(fd)
                     tty.setraw(fd)
+                    tty.setraw(stdout.fileno())
 
-                    # Enable a few things turned off by tty.setraw()
-                    # ICANON -> Allows characters to be deleted and hides things like ^M.
-                    # ICRNL -> Makes the return key work when ICANON is enabled, otherwise
-                    #          you get stuck at the prompt with no way to get out of it.
-                    # See man termios for details on these flags
-                    if not seconds:
+                    # Only echo input if no timeout is specified
+                    if not seconds and echo:
                         new_settings = termios.tcgetattr(fd)
-                        new_settings[0] = new_settings[0] | termios.ICRNL
-                        new_settings[3] = new_settings[3] | termios.ICANON
+                        new_settings[3] = new_settings[3] | termios.ECHO
                         termios.tcsetattr(fd, termios.TCSANOW, new_settings)
 
-                        if echo:
-                            # Enable ECHO since tty.setraw() disables it
-                            new_settings = termios.tcgetattr(fd)
-                            new_settings[3] = new_settings[3] | termios.ECHO
-                            termios.tcsetattr(fd, termios.TCSANOW, new_settings)
-
                     # flush the buffer to make sure no previous key presses
                     # are read in below
                     termios.tcflush(stdin, termios.TCIFLUSH)
+
             while True:
                 try:
                     if fd is not None:
                         key_pressed = stdin.read(1)
-
-                        if seconds:
-                            if key_pressed == b'\x03':
-                                raise KeyboardInterrupt
+                        if key_pressed == intr:  # value for Ctrl+C
+                            clear_line(stdout)
+                            raise KeyboardInterrupt
 
                     if not seconds:
                         if fd is None or not isatty(fd):
-                            display.warning("Not waiting from prompt as stdin is not interactive")
+                            display.warning("Not waiting for response to prompt as stdin is not interactive")
                             break
+
                         # read key presses and act accordingly
                         if key_pressed in (b'\r', b'\n'):
+                            clear_line(stdout)
                             break
+                        elif key_pressed in backspace:
+                            # delete a character if backspace is pressed
+                            result['user_input'] = result['user_input'][:-1]
+                            clear_line(stdout)
+                            if echo:
+                                stdout.write(result['user_input'])
+                            stdout.flush()
                         else:
                             result['user_input'] += key_pressed
 
                 except KeyboardInterrupt:
-                    if seconds is not None:
-                        signal.alarm(0)
+                    signal.alarm(0)
                     display.display("Press 'C' to continue the play or 'A' to abort \r"),
                     if self._c_or_a(stdin):
+                        clear_line(stdout)
                         break
-                    else:
-                        raise AnsibleError('user requested abort!')
+
+                    clear_line(stdout)
+
+                    raise AnsibleError('user requested abort!')
 
         except AnsibleTimeoutExceeded:
             # this is the exception we expect when the alarm signal


Test patch:
diff --git a/test/integration/targets/pause/aliases b/test/integration/targets/pause/aliases
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/pause/aliases
@@ -0,0 +1 @@
+posix/ci/group2
diff --git a/test/integration/targets/pause/pause-1.yml b/test/integration/targets/pause/pause-1.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/pause/pause-1.yml
@@ -0,0 +1,11 @@
+- name: Test pause module in default state
+  hosts: testhost
+  become: no
+  gather_facts: no
+
+  tasks:
+    - name: EXPECTED FAILURE
+      pause:
+
+    - debug:
+        msg: Task after pause
diff --git a/test/integration/targets/pause/pause-2.yml b/test/integration/targets/pause/pause-2.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/pause/pause-2.yml
@@ -0,0 +1,12 @@
+- name: Test pause module with custom prompt
+  hosts: testhost
+  become: no
+  gather_facts: no
+
+  tasks:
+    - name: EXPECTED FAILURE
+      pause:
+        prompt: Custom prompt
+
+    - debug:
+        msg: Task after pause
diff --git a/test/integration/targets/pause/pause-3.yml b/test/integration/targets/pause/pause-3.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/pause/pause-3.yml
@@ -0,0 +1,12 @@
+- name: Test pause module with pause
+  hosts: testhost
+  become: no
+  gather_facts: no
+
+  tasks:
+    - name: EXPECTED FAILURE
+      pause:
+        seconds: 2
+
+    - debug:
+        msg: Task after pause
diff --git a/test/integration/targets/pause/pause-4.yml b/test/integration/targets/pause/pause-4.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/pause/pause-4.yml
@@ -0,0 +1,13 @@
+- name: Test pause module with pause and custom prompt
+  hosts: testhost
+  become: no
+  gather_facts: no
+
+  tasks:
+    - name: EXPECTED FAILURE
+      pause:
+        seconds: 2
+        prompt: Waiting for two seconds
+
+    - debug:
+        msg: Task after pause
diff --git a/test/integration/targets/pause/pause-5.yml b/test/integration/targets/pause/pause-5.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/pause/pause-5.yml
@@ -0,0 +1,35 @@
+- name: Test pause module echo output
+  hosts: testhost
+  become: no
+  gather_facts: no
+
+  tasks:
+    - pause:
+        echo: yes
+        prompt: Enter some text
+      register: results
+
+    - name: Ensure that input was captured
+      assert:
+        that:
+          - results.user_input == 'hello there'
+
+    - pause:
+        echo: yes
+        prompt: Enter some text to edit
+      register: result
+
+    - name: Ensure edited input was captured
+      assert:
+        that:
+          - result.user_input == 'hello tommy boy'
+
+    - pause:
+        echo: no
+        prompt: Enter some text
+      register: result
+
+    - name: Ensure secret input was caputered
+      assert:
+        that:
+          - result.user_input == 'supersecretpancakes'
diff --git a/test/integration/targets/pause/runme.sh b/test/integration/targets/pause/runme.sh
new file mode 100755
--- /dev/null
+++ b/test/integration/targets/pause/runme.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+set -eux
+
+# Test pause module when no tty and non-interactive. This is to prevent playbooks
+# from hanging in cron and Tower jobs.
+/usr/bin/env bash << EOF
+ansible-playbook test-pause-no-tty.yml -i ../../inventory 2>&1 | \
+    grep '\[WARNING\]: Not waiting for response to prompt as stdin is not interactive' && {
+        echo 'Successfully skipped pause in no TTY mode' >&2
+        exit 0
+    } || {
+        echo 'Failed to skip pause module' >&2
+        exit 1
+    }
+EOF
+
+# Test pause with seconds and minutes specified
+ansible-playbook test-pause.yml -i ../../inventory "$@"
+
+# Interactively test pause
+pip install pexpect
+python test-pause.py -i ../../inventory "$@"
diff --git a/test/integration/targets/pause/test-pause-no-tty.yml b/test/integration/targets/pause/test-pause-no-tty.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/pause/test-pause-no-tty.yml
@@ -0,0 +1,7 @@
+- name: Test pause
+  hosts: testhost
+  gather_facts: no
+  become: no
+
+  tasks:
+    - pause:
diff --git a/test/integration/targets/pause/test-pause.py b/test/integration/targets/pause/test-pause.py
new file mode 100755
--- /dev/null
+++ b/test/integration/targets/pause/test-pause.py
@@ -0,0 +1,270 @@
+#!/usr/bin/env python
+
+import os
+import pexpect
+import sys
+import termios
+
+from ansible.module_utils.six import PY2
+
+args = sys.argv[1:]
+
+env_vars = {
+    'ANSIBLE_ROLES_PATH': './roles',
+    'ANSIBLE_NOCOLOR': 'True',
+    'ANSIBLE_RETRY_FILES_ENABLED': 'False'
+}
+
+try:
+    backspace = termios.tcgetattr(sys.stdin.fileno())[6][termios.VERASE]
+except Exception:
+    backspace = b'\x7f'
+
+if PY2:
+    log_buffer = sys.stdout
+else:
+    log_buffer = sys.stdout.buffer
+
+os.environ.update(env_vars)
+
+# -- Plain pause -- #
+playbook = 'pause-1.yml'
+
+# Case 1 - Contiune with enter
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Press enter to continue, Ctrl\+C to interrupt:')
+pause_test.send('\r')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 2 - Continue with C
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Press enter to continue, Ctrl\+C to interrupt:')
+pause_test.send('\x03')
+pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+pause_test.send('C')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 3 - Abort with A
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Press enter to continue, Ctrl\+C to interrupt:')
+pause_test.send('\x03')
+pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+pause_test.send('A')
+pause_test.expect('user requested abort!')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# -- Custom Prompt -- #
+playbook = 'pause-2.yml'
+
+# Case 1 - Contiune with enter
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Custom prompt:')
+pause_test.send('\r')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 2 - Contiune with C
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Custom prompt:')
+pause_test.send('\x03')
+pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+pause_test.send('C')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 3 - Abort with A
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Custom prompt:')
+pause_test.send('\x03')
+pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+pause_test.send('A')
+pause_test.expect('user requested abort!')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# -- Pause for N seconds -- #
+
+playbook = 'pause-3.yml'
+
+# Case 1 - Wait for task to continue after timeout
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# Case 2 - Contiune with Ctrl + C, C
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.send('\x03')
+pause_test.send('C')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 3 - Abort with Ctrl + C, A
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.send('\x03')
+pause_test.send('A')
+pause_test.expect('user requested abort!')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# -- Pause for N seconds with custom prompt -- #
+
+playbook = 'pause-4.yml'
+
+# Case 1 - Wait for task to continue after timeout
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.expect(r"Waiting for two seconds:")
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# Case 2 - Contiune with Ctrl + C, C
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.expect(r"Waiting for two seconds:")
+pause_test.send('\x03')
+pause_test.send('C')
+pause_test.expect('Task after pause')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+
+# Case 3 - Abort with Ctrl + C, A
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Pausing for \d+ seconds')
+pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+pause_test.expect(r"Waiting for two seconds:")
+pause_test.send('\x03')
+pause_test.send('A')
+pause_test.expect('user requested abort!')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
+
+# -- Enter input and ensure it's caputered, echoed, and can be edited -- #
+
+playbook = 'pause-5.yml'
+
+pause_test = pexpect.spawn(
+    'ansible-playbook',
+    args=[playbook] + args,
+    timeout=10,
+    env=os.environ
+)
+
+pause_test.logfile = log_buffer
+pause_test.expect(r'Enter some text:')
+pause_test.sendline('hello there')
+pause_test.expect(r'Enter some text to edit:')
+pause_test.send('hello there')
+pause_test.send(backspace * 4)
+pause_test.send('ommy boy\r')
+pause_test.expect(r'Enter some text \(output is hidden\):')
+pause_test.sendline('supersecretpancakes')
+pause_test.expect(pexpect.EOF)
+pause_test.close()
diff --git a/test/integration/targets/pause/test-pause.yml b/test/integration/targets/pause/test-pause.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/pause/test-pause.yml
@@ -0,0 +1,21 @@
+- name: Test pause
+  hosts: testhost
+  gather_facts: no
+  become: no
+
+  tasks:
+    - pause:
+        seconds: 1
+      register: results
+
+    - assert:
+        that:
+          - results.stdout is search('Paused for \d+\.\d+ seconds')
+
+    - pause:
+        minutes: 1
+      register: results
+
+    - assert:
+        that:
+          - results.stdout is search('Paused for \d+\.\d+ minutes')