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