summaryrefslogtreecommitdiff
path: root/lib/ansible/plugins/action/pause.py
blob: c261b37758e07368687934335cc94f52713aaf8c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# Copyright 2012, Tim Bielawa <tbielawa@redhat.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import datetime
import signal
import termios
import time
import tty

from os import isatty
from ansible.errors import AnsibleError
from ansible.plugins.action import ActionBase


class AnsibleTimeoutExceeded(Exception):
    pass


def timeout_handler(signum, frame):
    raise AnsibleTimeoutExceeded


class ActionModule(ActionBase):
    ''' pauses execution for a length or time, or until input is received '''

    PAUSE_TYPES = ['seconds', 'minutes', 'prompt', '']
    BYPASS_HOST_LOOP = True

    def run(self, tmp=None, task_vars=None):
        ''' run the pause action module '''
        if task_vars is None:
            task_vars = dict()

        result = super(ActionModule, self).run(tmp, task_vars)

        duration_unit = 'minutes'
        prompt = None
        seconds = None
        result.update(dict(
            changed = False,
            rc      = 0,
            stderr  = '',
            stdout  = '',
            start   = None,
            stop    = None,
            delta   = None,
        ))

        # 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()

        # Are 'minutes' or 'seconds' keys that exist in 'args'?
        elif '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
                    # recalculate for minutes=X values.
                    seconds = int(self._task.args['minutes']) * 60
                else:
                    seconds = int(self._task.args['seconds'])
                    duration_unit = 'seconds'

            except ValueError as e:
                result['failed'] = True
                result['msg'] = "non-integer value given for prompt duration:\n%s" % str(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!

        start = time.time()
        result['start'] = str(datetime.datetime.now())
        result['user_input'] = ''

        try:
            if seconds is not None:
                # setup the alarm handler
                signal.signal(signal.SIGALRM, timeout_handler)
                signal.alarm(seconds)
                # show the prompt
                self._display.display("Pausing for %d seconds" % seconds)
                self._display.display("(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r"),
            else:
                self._display.display(prompt)

            # save the attributes on the existing (duped) stdin so
            # that we can restore them later after we set raw mode
            fd = self._connection._new_stdin.fileno()
            if isatty(fd):
                old_settings = termios.tcgetattr(fd)
                tty.setraw(fd)

                # flush the buffer to make sure no previous key presses
                # are read in below
                termios.tcflush(self._connection._new_stdin, termios.TCIFLUSH)

            while True:
                try:
                    key_pressed = self._connection._new_stdin.read(1)
                    if key_pressed == '\x03':
                        raise KeyboardInterrupt

                    if not seconds:
                        if not isatty(fd):
                            self._display.warning("Not waiting from prompt as stdin is not interactive")
                            break
                        # read key presses and act accordingly
                        if key_pressed == '\r':
                            break
                        else:
                            result['user_input'] += key_pressed

                except KeyboardInterrupt:
                    if seconds is not None:
                        signal.alarm(0)
                    self._display.display("Press 'C' to continue the play or 'A' to abort \r"),
                    if self._c_or_a():
                        break
                    else:
                        raise AnsibleError('user requested abort!')

        except AnsibleTimeoutExceeded:
            # this is the exception we expect when the alarm signal
            # fires, so we simply ignore it to move into the cleanup
            pass
        finally:
            # cleanup and save some information
            # restore the old settings for the duped stdin fd
            if isatty(fd):
                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

            duration = time.time() - start
            result['stop'] = str(datetime.datetime.now())
            result['delta'] = int(duration)

            if duration_unit == 'minutes':
                duration = round(duration / 60.0, 2)
            else:
                duration = round(duration, 2)
            result['stdout'] = "Paused for %s %s" % (duration, duration_unit)

        return result

    def _c_or_a(self):
        while True:
            key_pressed = self._connection._new_stdin.read(1)
            if key_pressed.lower() == 'a':
                return False
            elif key_pressed.lower() == 'c':
                return True