1# This code is part of Ansible, but is an independent component.
2# This particular file snippet, and this file snippet only, is BSD licensed.
3# Modules you write using this snippet, which is embedded dynamically by Ansible
4# still belong to the author of the module, and may assign their own license
5# to the complete work.
6#
7# Copyright (c) 2015 Peter Sprygada, <psprygada@ansible.com>
8#
9# Redistribution and use in source and binary forms, with or without modification,
10# are permitted provided that the following conditions are met:
11#
12#    * Redistributions of source code must retain the above copyright
13#      notice, this list of conditions and the following disclaimer.
14#    * Redistributions in binary form must reproduce the above copyright notice,
15#      this list of conditions and the following disclaimer in the documentation
16#      and/or other materials provided with the distribution.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
22# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
26# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28import re
29import shlex
30import time
31
32from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE, BOOLEANS_FALSE
33from ansible.module_utils.six import string_types, text_type
34from ansible.module_utils.six.moves import zip
35
36
37def to_list(val):
38    if isinstance(val, (list, tuple)):
39        return list(val)
40    elif val is not None:
41        return [val]
42    else:
43        return list()
44
45
46class FailedConditionsError(Exception):
47    def __init__(self, msg, failed_conditions):
48        super(FailedConditionsError, self).__init__(msg)
49        self.failed_conditions = failed_conditions
50
51
52class FailedConditionalError(Exception):
53    def __init__(self, msg, failed_conditional):
54        super(FailedConditionalError, self).__init__(msg)
55        self.failed_conditional = failed_conditional
56
57
58class AddCommandError(Exception):
59    def __init__(self, msg, command):
60        super(AddCommandError, self).__init__(msg)
61        self.command = command
62
63
64class AddConditionError(Exception):
65    def __init__(self, msg, condition):
66        super(AddConditionError, self).__init__(msg)
67        self.condition = condition
68
69
70class Cli(object):
71
72    def __init__(self, connection):
73        self.connection = connection
74        self.default_output = connection.default_output or 'text'
75        self._commands = list()
76
77    @property
78    def commands(self):
79        return [str(c) for c in self._commands]
80
81    def __call__(self, commands, output=None):
82        objects = list()
83        for cmd in to_list(commands):
84            objects.append(self.to_command(cmd, output))
85        return self.connection.run_commands(objects)
86
87    def to_command(self, command, output=None, prompt=None, response=None, **kwargs):
88        output = output or self.default_output
89        if isinstance(command, Command):
90            return command
91        if isinstance(prompt, string_types):
92            prompt = re.compile(re.escape(prompt))
93        return Command(command, output, prompt=prompt, response=response, **kwargs)
94
95    def add_commands(self, commands, output=None, **kwargs):
96        for cmd in commands:
97            self._commands.append(self.to_command(cmd, output, **kwargs))
98
99    def run_commands(self):
100        responses = self.connection.run_commands(self._commands)
101        for resp, cmd in zip(responses, self._commands):
102            cmd.response = resp
103
104        # wipe out the commands list to avoid issues if additional
105        # commands are executed later
106        self._commands = list()
107
108        return responses
109
110
111class Command(object):
112
113    def __init__(self, command, output=None, prompt=None, response=None,
114                 **kwargs):
115
116        self.command = command
117        self.output = output
118        self.command_string = command
119
120        self.prompt = prompt
121        self.response = response
122
123        self.args = kwargs
124
125    def __str__(self):
126        return self.command_string
127
128
129class CommandRunner(object):
130
131    def __init__(self, module):
132        self.module = module
133
134        self.items = list()
135        self.conditionals = set()
136
137        self.commands = list()
138
139        self.retries = 10
140        self.interval = 1
141
142        self.match = 'all'
143
144        self._default_output = module.connection.default_output
145
146    def add_command(self, command, output=None, prompt=None, response=None,
147                    **kwargs):
148        if command in [str(c) for c in self.commands]:
149            raise AddCommandError('duplicated command detected', command=command)
150        cmd = self.module.cli.to_command(command, output=output, prompt=prompt,
151                                         response=response, **kwargs)
152        self.commands.append(cmd)
153
154    def get_command(self, command, output=None):
155        for cmd in self.commands:
156            if cmd.command == command:
157                return cmd.response
158        raise ValueError("command '%s' not found" % command)
159
160    def get_responses(self):
161        return [cmd.response for cmd in self.commands]
162
163    def add_conditional(self, condition):
164        try:
165            self.conditionals.add(Conditional(condition))
166        except AttributeError as exc:
167            raise AddConditionError(msg=str(exc), condition=condition)
168
169    def run(self):
170        while self.retries > 0:
171            self.module.cli.add_commands(self.commands)
172            responses = self.module.cli.run_commands()
173
174            for item in list(self.conditionals):
175                if item(responses):
176                    if self.match == 'any':
177                        return item
178                    self.conditionals.remove(item)
179
180            if not self.conditionals:
181                break
182
183            time.sleep(self.interval)
184            self.retries -= 1
185        else:
186            failed_conditions = [item.raw for item in self.conditionals]
187            errmsg = 'One or more conditional statements have not been satisfied'
188            raise FailedConditionsError(errmsg, failed_conditions)
189
190
191class Conditional(object):
192    """Used in command modules to evaluate waitfor conditions
193    """
194
195    OPERATORS = {
196        'eq': ['eq', '=='],
197        'neq': ['neq', 'ne', '!='],
198        'gt': ['gt', '>'],
199        'ge': ['ge', '>='],
200        'lt': ['lt', '<'],
201        'le': ['le', '<='],
202        'contains': ['contains'],
203        'matches': ['matches']
204    }
205
206    def __init__(self, conditional, encoding=None):
207        self.raw = conditional
208        self.negate = False
209        try:
210            components = shlex.split(conditional)
211            key, val = components[0], components[-1]
212            op_components = components[1:-1]
213            if 'not' in op_components:
214                self.negate = True
215                op_components.pop(op_components.index('not'))
216            op = op_components[0]
217
218        except ValueError:
219            raise ValueError('failed to parse conditional')
220
221        self.key = key
222        self.func = self._func(op)
223        self.value = self._cast_value(val)
224
225    def __call__(self, data):
226        value = self.get_value(dict(result=data))
227        if not self.negate:
228            return self.func(value)
229        else:
230            return not self.func(value)
231
232    def _cast_value(self, value):
233        if value in BOOLEANS_TRUE:
234            return True
235        elif value in BOOLEANS_FALSE:
236            return False
237        elif re.match(r'^\d+\.d+$', value):
238            return float(value)
239        elif re.match(r'^\d+$', value):
240            return int(value)
241        else:
242            return text_type(value)
243
244    def _func(self, oper):
245        for func, operators in self.OPERATORS.items():
246            if oper in operators:
247                return getattr(self, func)
248        raise AttributeError('unknown operator: %s' % oper)
249
250    def get_value(self, result):
251        try:
252            return self.get_json(result)
253        except (IndexError, TypeError, AttributeError):
254            msg = 'unable to apply conditional to result'
255            raise FailedConditionalError(msg, self.raw)
256
257    def get_json(self, result):
258        string = re.sub(r"\[[\'|\"]", ".", self.key)
259        string = re.sub(r"[\'|\"]\]", ".", string)
260        parts = re.split(r'\.(?=[^\]]*(?:\[|$))', string)
261        for part in parts:
262            match = re.findall(r'\[(\S+?)\]', part)
263            if match:
264                key = part[:part.find('[')]
265                result = result[key]
266                for m in match:
267                    try:
268                        m = int(m)
269                    except ValueError:
270                        m = str(m)
271                    result = result[m]
272            else:
273                result = result.get(part)
274        return result
275
276    def number(self, value):
277        if '.' in str(value):
278            return float(value)
279        else:
280            return int(value)
281
282    def eq(self, value):
283        return value == self.value
284
285    def neq(self, value):
286        return value != self.value
287
288    def gt(self, value):
289        return self.number(value) > self.value
290
291    def ge(self, value):
292        return self.number(value) >= self.value
293
294    def lt(self, value):
295        return self.number(value) < self.value
296
297    def le(self, value):
298        return self.number(value) <= self.value
299
300    def contains(self, value):
301        return str(self.value) in value
302
303    def matches(self, value):
304        match = re.search(self.value, value, re.M)
305        return match is not None
306