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 (
33    BOOLEANS_TRUE,
34    BOOLEANS_FALSE,
35)
36from ansible.module_utils.six import string_types, text_type
37from ansible.module_utils.six.moves import zip
38
39
40def to_list(val):
41    if isinstance(val, (list, tuple)):
42        return list(val)
43    elif val is not None:
44        return [val]
45    else:
46        return list()
47
48
49class FailedConditionsError(Exception):
50    def __init__(self, msg, failed_conditions):
51        super(FailedConditionsError, self).__init__(msg)
52        self.failed_conditions = failed_conditions
53
54
55class FailedConditionalError(Exception):
56    def __init__(self, msg, failed_conditional):
57        super(FailedConditionalError, self).__init__(msg)
58        self.failed_conditional = failed_conditional
59
60
61class AddCommandError(Exception):
62    def __init__(self, msg, command):
63        super(AddCommandError, self).__init__(msg)
64        self.command = command
65
66
67class AddConditionError(Exception):
68    def __init__(self, msg, condition):
69        super(AddConditionError, self).__init__(msg)
70        self.condition = condition
71
72
73class Cli(object):
74    def __init__(self, connection):
75        self.connection = connection
76        self.default_output = connection.default_output or "text"
77        self._commands = list()
78
79    @property
80    def commands(self):
81        return [str(c) for c in self._commands]
82
83    def __call__(self, commands, output=None):
84        objects = list()
85        for cmd in to_list(commands):
86            objects.append(self.to_command(cmd, output))
87        return self.connection.run_commands(objects)
88
89    def to_command(
90        self, command, output=None, prompt=None, response=None, **kwargs
91    ):
92        output = output or self.default_output
93        if isinstance(command, Command):
94            return command
95        if isinstance(prompt, string_types):
96            prompt = re.compile(re.escape(prompt))
97        return Command(
98            command, output, prompt=prompt, response=response, **kwargs
99        )
100
101    def add_commands(self, commands, output=None, **kwargs):
102        for cmd in commands:
103            self._commands.append(self.to_command(cmd, output, **kwargs))
104
105    def run_commands(self):
106        responses = self.connection.run_commands(self._commands)
107        for resp, cmd in zip(responses, self._commands):
108            cmd.response = resp
109
110        # wipe out the commands list to avoid issues if additional
111        # commands are executed later
112        self._commands = list()
113
114        return responses
115
116
117class Command(object):
118    def __init__(
119        self, command, output=None, prompt=None, response=None, **kwargs
120    ):
121
122        self.command = command
123        self.output = output
124        self.command_string = command
125
126        self.prompt = prompt
127        self.response = response
128
129        self.args = kwargs
130
131    def __str__(self):
132        return self.command_string
133
134
135class CommandRunner(object):
136    def __init__(self, module):
137        self.module = module
138
139        self.items = list()
140        self.conditionals = set()
141
142        self.commands = list()
143
144        self.retries = 10
145        self.interval = 1
146
147        self.match = "all"
148
149        self._default_output = module.connection.default_output
150
151    def add_command(
152        self, command, output=None, prompt=None, response=None, **kwargs
153    ):
154        if command in [str(c) for c in self.commands]:
155            raise AddCommandError(
156                "duplicated command detected", command=command
157            )
158        cmd = self.module.cli.to_command(
159            command, output=output, prompt=prompt, response=response, **kwargs
160        )
161        self.commands.append(cmd)
162
163    def get_command(self, command, output=None):
164        for cmd in self.commands:
165            if cmd.command == command:
166                return cmd.response
167        raise ValueError("command '%s' not found" % command)
168
169    def get_responses(self):
170        return [cmd.response for cmd in self.commands]
171
172    def add_conditional(self, condition):
173        try:
174            self.conditionals.add(Conditional(condition))
175        except AttributeError as exc:
176            raise AddConditionError(msg=str(exc), condition=condition)
177
178    def run(self):
179        while self.retries > 0:
180            self.module.cli.add_commands(self.commands)
181            responses = self.module.cli.run_commands()
182
183            for item in list(self.conditionals):
184                if item(responses):
185                    if self.match == "any":
186                        return item
187                    self.conditionals.remove(item)
188
189            if not self.conditionals:
190                break
191
192            time.sleep(self.interval)
193            self.retries -= 1
194        else:
195            failed_conditions = [item.raw for item in self.conditionals]
196            errmsg = (
197                "One or more conditional statements have not been satisfied"
198            )
199            raise FailedConditionsError(errmsg, failed_conditions)
200
201
202class Conditional(object):
203    """Used in command modules to evaluate waitfor conditions
204    """
205
206    OPERATORS = {
207        "eq": ["eq", "=="],
208        "neq": ["neq", "ne", "!="],
209        "gt": ["gt", ">"],
210        "ge": ["ge", ">="],
211        "lt": ["lt", "<"],
212        "le": ["le", "<="],
213        "contains": ["contains"],
214        "matches": ["matches"],
215    }
216
217    def __init__(self, conditional, encoding=None):
218        self.raw = conditional
219        self.negate = False
220        try:
221            components = shlex.split(conditional)
222            key, val = components[0], components[-1]
223            op_components = components[1:-1]
224            if "not" in op_components:
225                self.negate = True
226                op_components.pop(op_components.index("not"))
227            op = op_components[0]
228
229        except ValueError:
230            raise ValueError("failed to parse conditional")
231
232        self.key = key
233        self.func = self._func(op)
234        self.value = self._cast_value(val)
235
236    def __call__(self, data):
237        value = self.get_value(dict(result=data))
238        if not self.negate:
239            return self.func(value)
240        else:
241            return not self.func(value)
242
243    def _cast_value(self, value):
244        if value in BOOLEANS_TRUE:
245            return True
246        elif value in BOOLEANS_FALSE:
247            return False
248        elif re.match(r"^\d+\.d+$", value):
249            return float(value)
250        elif re.match(r"^\d+$", value):
251            return int(value)
252        else:
253            return text_type(value)
254
255    def _func(self, oper):
256        for func, operators in self.OPERATORS.items():
257            if oper in operators:
258                return getattr(self, func)
259        raise AttributeError("unknown operator: %s" % oper)
260
261    def get_value(self, result):
262        try:
263            return self.get_json(result)
264        except (IndexError, TypeError, AttributeError):
265            msg = "unable to apply conditional to result"
266            raise FailedConditionalError(msg, self.raw)
267
268    def get_json(self, result):
269        string = re.sub(r"\[[\'|\"]", ".", self.key)
270        string = re.sub(r"[\'|\"]\]", ".", string)
271        parts = re.split(r"\.(?=[^\]]*(?:\[|$))", string)
272        for part in parts:
273            match = re.findall(r"\[(\S+?)\]", part)
274            if match:
275                key = part[: part.find("[")]
276                result = result[key]
277                for m in match:
278                    try:
279                        m = int(m)
280                    except ValueError:
281                        m = str(m)
282                    result = result[m]
283            else:
284                result = result.get(part)
285        return result
286
287    def number(self, value):
288        if "." in str(value):
289            return float(value)
290        else:
291            return int(value)
292
293    def eq(self, value):
294        return value == self.value
295
296    def neq(self, value):
297        return value != self.value
298
299    def gt(self, value):
300        return self.number(value) > self.value
301
302    def ge(self, value):
303        return self.number(value) >= self.value
304
305    def lt(self, value):
306        return self.number(value) < self.value
307
308    def le(self, value):
309        return self.number(value) <= self.value
310
311    def contains(self, value):
312        return str(self.value) in value
313
314    def matches(self, value):
315        match = re.search(self.value, value, re.M)
316        return match is not None
317