Skip to content

Commit e65045f

Browse files
samdoranbcoca
authored andcommitted
Add echo option to pause module (ansible#32205)
* Enable ECHO in prompt module Fixes ansible#14160 * Add option for controlling echo behavior with pause module * Improve option logic Allow all options to be used in varying combinations, rather than being mutually exclusive. Always capture output and return it, even when a time limit is set. * Add version_added to docs * Improve behavior of echo output Set a few more flags to allow interactive deletion and hide control characters. Do not capture or echo input when a time is set. Tried to get this working nicely, but ran into too many issues/oddities to keep it. Maybe in the future if there is demand for capturing/echoing input when a time is set I'll take another pass at it.
1 parent 6ce3972 commit e65045f

File tree

2 files changed

+67
-23
lines changed

2 files changed

+67
-23
lines changed

lib/ansible/modules/utilities/logic/pause.py

+19
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,19 @@
4040
- Optional text to use for the prompt message.
4141
required: false
4242
default: null
43+
echo:
44+
description:
45+
- Contols whether or not keyboard input is shown when typing.
46+
- Has no effect if 'seconds' or 'minutes' is set.
47+
required: false
48+
default: 'yes'
49+
choices: ['yes', 'no']
50+
version_added: 2.5
4351
author: "Tim Bielawa (@tbielawa)"
4452
notes:
4553
- 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.
4654
- This module is also supported for Windows targets.
55+
- User input is not captured or echoed, regardless of echo setting, when minutes or seconds is specified.
4756
'''
4857

4958
EXAMPLES = '''
@@ -57,6 +66,11 @@
5766
# A helpful reminder of what to look out for post-update.
5867
- pause:
5968
prompt: "Make sure org.foo.FooOverload exception is not present"
69+
70+
# Pause to get some sensitive input.
71+
- pause:
72+
prompt: "Enter a secret"
73+
echo: no
6074
'''
6175

6276
RETURN = '''
@@ -85,4 +99,9 @@
8599
returned: always
86100
type: string
87101
sample: Paused for 0.04 minutes
102+
echo:
103+
description: Value of echo setting
104+
returned: always
105+
type: bool
106+
sample: true
88107
'''

lib/ansible/plugins/action/pause.py

+48-23
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def timeout_handler(signum, frame):
4747
class ActionModule(ActionBase):
4848
''' pauses execution for a length or time, or until input is received '''
4949

50-
PAUSE_TYPES = ['seconds', 'minutes', 'prompt', '']
50+
PAUSE_TYPES = ['seconds', 'minutes', 'prompt', 'echo', '']
5151
BYPASS_HOST_LOOP = True
5252

5353
def run(self, tmp=None, task_vars=None):
@@ -60,6 +60,8 @@ def run(self, tmp=None, task_vars=None):
6060
duration_unit = 'minutes'
6161
prompt = None
6262
seconds = None
63+
echo = True
64+
echo_prompt = ''
6365
result.update(dict(
6466
changed=False,
6567
rc=0,
@@ -68,14 +70,35 @@ def run(self, tmp=None, task_vars=None):
6870
start=None,
6971
stop=None,
7072
delta=None,
73+
echo=echo
7174
))
7275

73-
# Is 'args' empty, then this is the default prompted pause
74-
if self._task.args is None or len(self._task.args.keys()) == 0:
75-
prompt = "[%s]\nPress enter to continue:" % self._task.get_name().strip()
76+
if not set(self._task.args.keys()) <= set(self.PAUSE_TYPES):
77+
result['failed'] = True
78+
result['msg'] = "Invalid argument given. Must be one of: %s" % ", ".join(self.PAUSE_TYPES)
79+
return result
80+
81+
# Should keystrokes be echoed to stdout?
82+
if 'echo' in self._task.args:
83+
echo = self._task.args['echo']
84+
if not type(echo) == bool:
85+
result['failed'] = True
86+
result['msg'] = "'%s' is not a valid setting for 'echo'." % self._task.args['echo']
87+
return result
88+
89+
# Add a note saying the output is hidden if echo is disabled
90+
if not echo:
91+
echo_prompt = ' (output is hidden)'
92+
93+
# Is 'prompt' a key in 'args'?
94+
if 'prompt' in self._task.args:
95+
prompt = "[%s]\n%s%s:" % (self._task.get_name().strip(), self._task.args['prompt'], echo_prompt)
96+
else:
97+
# If no custom prompt is specified, set a default prompt
98+
prompt = "[%s]\n%s%s:" % (self._task.get_name().strip(), 'Press enter to continue', echo_prompt)
7699

77100
# Are 'minutes' or 'seconds' keys that exist in 'args'?
78-
elif 'minutes' in self._task.args or 'seconds' in self._task.args:
101+
if 'minutes' in self._task.args or 'seconds' in self._task.args:
79102
try:
80103
if 'minutes' in self._task.args:
81104
# The time() command operates in seconds so we need to
@@ -90,16 +113,6 @@ def run(self, tmp=None, task_vars=None):
90113
result['msg'] = u"non-integer value given for prompt duration:\n%s" % to_text(e)
91114
return result
92115

93-
# Is 'prompt' a key in 'args'?
94-
elif 'prompt' in self._task.args:
95-
prompt = "[%s]\n%s:" % (self._task.get_name().strip(), self._task.args['prompt'])
96-
97-
else:
98-
# I have no idea what you're trying to do. But it's so wrong.
99-
result['failed'] = True
100-
result['msg'] = "invalid pause type given. must be one of: %s" % ", ".join(self.PAUSE_TYPES)
101-
return result
102-
103116
########################################################################
104117
# Begin the hard work!
105118

@@ -113,12 +126,19 @@ def run(self, tmp=None, task_vars=None):
113126
if seconds is not None:
114127
if seconds < 1:
115128
seconds = 1
129+
116130
# setup the alarm handler
117131
signal.signal(signal.SIGALRM, timeout_handler)
118132
signal.alarm(seconds)
119-
# show the prompt
120-
display.display("Pausing for %d seconds" % seconds)
133+
134+
# show the timer and control prompts
135+
display.display("Pausing for %d seconds%s" % (seconds, echo_prompt))
121136
display.display("(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r"),
137+
138+
# show the prompt specified in the task
139+
if 'prompt' in self._task.args:
140+
display.display(prompt)
141+
122142
else:
123143
display.display(prompt)
124144

@@ -144,25 +164,30 @@ def run(self, tmp=None, task_vars=None):
144164
# ICANON -> Allows characters to be deleted and hides things like ^M.
145165
# ICRNL -> Makes the return key work when ICANON is enabled, otherwise
146166
# you get stuck at the prompt with no way to get out of it.
147-
# ECHO -> Echos input back to stdout
148-
#
149167
# See man termios for details on these flags
150168
if not seconds:
151169
new_settings = termios.tcgetattr(fd)
152170
new_settings[0] = new_settings[0] | termios.ICRNL
153-
new_settings[3] = new_settings[3] | (termios.ICANON | termios.ECHO)
154-
termios.tcsetattr(fd, termios.TCSANOW, new_settings)
171+
new_settings[3] = new_settings[3] | termios.ICANON
155172
termios.tcsetattr(fd, termios.TCSANOW, new_settings)
156173

174+
if echo:
175+
# Enable ECHO since tty.setraw() disables it
176+
new_settings = termios.tcgetattr(fd)
177+
new_settings[3] = new_settings[3] | termios.ECHO
178+
termios.tcsetattr(fd, termios.TCSANOW, new_settings)
179+
157180
# flush the buffer to make sure no previous key presses
158181
# are read in below
159182
termios.tcflush(stdin, termios.TCIFLUSH)
160183
while True:
161184
try:
162185
if fd is not None:
163186
key_pressed = stdin.read(1)
164-
if key_pressed == b'\x03':
165-
raise KeyboardInterrupt
187+
188+
if seconds:
189+
if key_pressed == b'\x03':
190+
raise KeyboardInterrupt
166191

167192
if not seconds:
168193
if fd is None or not isatty(fd):

0 commit comments

Comments
 (0)