diff --git a/lib/ansible/modules/utilities/logic/pause.py b/lib/ansible/modules/utilities/logic/pause.py index 6ecff6731a..a8cf2efbcb 100644 --- a/lib/ansible/modules/utilities/logic/pause.py +++ b/lib/ansible/modules/utilities/logic/pause.py @@ -40,10 +40,19 @@ options: - Optional text to use for the prompt message. required: false default: null + echo: + description: + - Contols whether or not keyboard input is shown when typing. + - Has no effect if 'seconds' or 'minutes' is set. + required: false + default: 'yes' + choices: ['yes', 'no'] + version_added: 2.5 author: "Tim Bielawa (@tbielawa)" notes: - Starting in 2.2, if you specify 0 or negative for minutes or seconds, it will wait for 1 second, previously it would wait indefinitely. - This module is also supported for Windows targets. + - User input is not captured or echoed, regardless of echo setting, when minutes or seconds is specified. ''' EXAMPLES = ''' @@ -57,6 +66,11 @@ EXAMPLES = ''' # A helpful reminder of what to look out for post-update. - pause: prompt: "Make sure org.foo.FooOverload exception is not present" + +# Pause to get some sensitive input. +- pause: + prompt: "Enter a secret" + echo: no ''' RETURN = ''' @@ -85,4 +99,9 @@ stdout: returned: always type: string sample: Paused for 0.04 minutes +echo: + description: Value of echo setting + returned: always + type: bool + sample: true ''' diff --git a/lib/ansible/plugins/action/pause.py b/lib/ansible/plugins/action/pause.py index abe7ee6ae1..300be6f18e 100644 --- a/lib/ansible/plugins/action/pause.py +++ b/lib/ansible/plugins/action/pause.py @@ -47,7 +47,7 @@ def timeout_handler(signum, frame): class ActionModule(ActionBase): ''' pauses execution for a length or time, or until input is received ''' - PAUSE_TYPES = ['seconds', 'minutes', 'prompt', ''] + PAUSE_TYPES = ['seconds', 'minutes', 'prompt', 'echo', ''] BYPASS_HOST_LOOP = True def run(self, tmp=None, task_vars=None): @@ -60,6 +60,8 @@ class ActionModule(ActionBase): duration_unit = 'minutes' prompt = None seconds = None + echo = True + echo_prompt = '' result.update(dict( changed=False, rc=0, @@ -68,14 +70,35 @@ class ActionModule(ActionBase): start=None, stop=None, delta=None, + echo=echo )) - # Is 'args' empty, then this is the default prompted pause - if self._task.args is None or len(self._task.args.keys()) == 0: - prompt = "[%s]\nPress enter to continue:" % self._task.get_name().strip() + if not set(self._task.args.keys()) <= set(self.PAUSE_TYPES): + result['failed'] = True + result['msg'] = "Invalid argument given. Must be one of: %s" % ", ".join(self.PAUSE_TYPES) + return result + + # Should keystrokes be echoed to stdout? + if 'echo' in self._task.args: + echo = self._task.args['echo'] + if not type(echo) == bool: + result['failed'] = True + result['msg'] = "'%s' is not a valid setting for 'echo'." % self._task.args['echo'] + return result + + # Add a note saying the output is hidden if echo is disabled + if not echo: + echo_prompt = ' (output is hidden)' + + # Is 'prompt' a key in 'args'? + if 'prompt' in self._task.args: + 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) # Are 'minutes' or 'seconds' keys that exist in 'args'? - elif 'minutes' in self._task.args or 'seconds' in self._task.args: + if 'minutes' in self._task.args or 'seconds' in self._task.args: try: if 'minutes' in self._task.args: # The time() command operates in seconds so we need to @@ -90,16 +113,6 @@ class ActionModule(ActionBase): result['msg'] = u"non-integer value given for prompt duration:\n%s" % to_text(e) return result - # Is 'prompt' a key in 'args'? - elif 'prompt' in self._task.args: - prompt = "[%s]\n%s:" % (self._task.get_name().strip(), self._task.args['prompt']) - - else: - # I have no idea what you're trying to do. But it's so wrong. - result['failed'] = True - result['msg'] = "invalid pause type given. must be one of: %s" % ", ".join(self.PAUSE_TYPES) - return result - ######################################################################## # Begin the hard work! @@ -113,12 +126,19 @@ class ActionModule(ActionBase): if seconds is not None: if seconds < 1: seconds = 1 + # setup the alarm handler signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(seconds) - # show the prompt - display.display("Pausing for %d seconds" % seconds) + + # show the timer and control prompts + display.display("Pausing for %d seconds%s" % (seconds, echo_prompt)) display.display("(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r"), + + # show the prompt specified in the task + if 'prompt' in self._task.args: + display.display(prompt) + else: display.display(prompt) @@ -144,16 +164,19 @@ class ActionModule(ActionBase): # 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. - # ECHO -> Echos input back to stdout - # # See man termios for details on these flags if not seconds: new_settings = termios.tcgetattr(fd) new_settings[0] = new_settings[0] | termios.ICRNL - new_settings[3] = new_settings[3] | (termios.ICANON | termios.ECHO) - termios.tcsetattr(fd, termios.TCSANOW, new_settings) + new_settings[3] = new_settings[3] | termios.ICANON 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) @@ -161,8 +184,10 @@ class ActionModule(ActionBase): try: if fd is not None: key_pressed = stdin.read(1) - if key_pressed == b'\x03': - raise KeyboardInterrupt + + if seconds: + if key_pressed == b'\x03': + raise KeyboardInterrupt if not seconds: if fd is None or not isatty(fd):