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