1""" 2Test States 3=========== 4 5Provide test case states that enable easy testing of things to do with state 6calls, e.g. running, calling, logging, output filtering etc. 7 8.. code-block:: yaml 9 10 always-passes-with-any-kwarg: 11 test.nop: 12 - name: foo 13 - something: else 14 - foo: bar 15 16 always-passes: 17 test.succeed_without_changes: 18 - name: foo 19 20 always-fails: 21 test.fail_without_changes: 22 - name: foo 23 24 always-changes-and-succeeds: 25 test.succeed_with_changes: 26 - name: foo 27 28 always-changes-and-fails: 29 test.fail_with_changes: 30 - name: foo 31 32 my-custom-combo: 33 test.configurable_test_state: 34 - name: foo 35 - changes: True 36 - result: False 37 - comment: bar.baz 38 - warnings: A warning 39 40 is-pillar-foo-present-and-bar-is-int: 41 test.check_pillar: 42 - present: 43 - foo 44 - integer: 45 - bar 46 47You may also use these states for controlled failure in state definitions, for example if certain conditions in 48pillar or grains do not apply. The following state definition will fail with a message "OS not supported!" when 49`grains['os']` is neither Ubuntu nor CentOS: 50 51.. code-block:: jinja 52 53 {% if grains['os'] in ['Ubuntu', 'CentOS'] %} 54 55 # Your state definitions go here 56 57 {% else %} 58 failure: 59 test.fail_without_changes: 60 - name: "OS not supported!" 61 - failhard: True 62 {% endif %} 63 64""" 65 66import random 67 68import salt.utils.data 69from salt.exceptions import SaltInvocationError 70from salt.state import _gen_tag 71 72 73def nop(name, **kwargs): 74 """ 75 .. versionadded:: 2015.8.1 76 77 A no-op state that does nothing. Useful in conjunction with the ``use`` 78 requisite, or in templates which could otherwise be empty due to jinja 79 rendering. 80 81 name 82 A unique string to serve as the state's ID 83 """ 84 return succeed_without_changes(name) 85 86 87def succeed_without_changes(name, **kwargs): # pylint: disable=unused-argument 88 """ 89 .. versionadded:: 2014.7.0 90 91 Returns successful 92 93 name 94 A unique string to serve as the state's ID 95 """ 96 comment = kwargs.get("comment", "Success!") 97 98 ret = {"name": name, "changes": {}, "result": True, "comment": comment} 99 return ret 100 101 102def fail_without_changes(name, **kwargs): # pylint: disable=unused-argument 103 """ 104 .. versionadded:: 2014.7.0 105 106 Returns failure 107 108 name 109 A unique string to serve as the state's ID 110 """ 111 comment = kwargs.get("comment", "Failure!") 112 113 ret = {"name": name, "changes": {}, "result": False, "comment": comment} 114 115 if __opts__["test"]: 116 ret["result"] = False 117 ret["comment"] = "If we weren't testing, this would be a failure!" 118 119 return ret 120 121 122def succeed_with_changes(name, **kwargs): # pylint: disable=unused-argument 123 """ 124 .. versionadded:: 2014.7.0 125 126 Returns ``True`` with an non-empty ``changes`` dictionary. Useful for 127 testing requisites. 128 129 name 130 A unique string to serve as the state's ID 131 """ 132 comment = kwargs.get("comment", "Success!") 133 134 ret = {"name": name, "changes": {}, "result": True, "comment": comment} 135 136 # Following the docs as written here 137 # https://docs.saltproject.io/ref/states/writing.html#return-data 138 ret["changes"] = { 139 "testing": {"old": "Unchanged", "new": "Something pretended to change"} 140 } 141 142 if __opts__["test"]: 143 ret["result"] = None 144 ret["comment"] = "If we weren't testing, this would be successful with changes" 145 146 return ret 147 148 149def fail_with_changes(name, **kwargs): # pylint: disable=unused-argument 150 """ 151 .. versionadded:: 2014.7.0 152 153 Returns ``False`` with an non-empty ``changes`` dictionary. Useful for 154 testing requisites. 155 156 name 157 A unique string to serve as the state's ID 158 """ 159 comment = kwargs.get("comment", "Failure!") 160 161 ret = {"name": name, "changes": {}, "result": False, "comment": comment} 162 163 # Following the docs as written here 164 # https://docs.saltproject.io/ref/states/writing.html#return-data 165 ret["changes"] = { 166 "testing": {"old": "Unchanged", "new": "Something pretended to change"} 167 } 168 169 if __opts__["test"]: 170 ret["result"] = None 171 ret["comment"] = "If we weren't testing, this would be failed with changes" 172 173 return ret 174 175 176def configurable_test_state(name, changes=True, result=True, comment="", warnings=None): 177 """ 178 .. versionadded:: 2014.7.0 179 180 A configurable test state which allows for more control over the return 181 data 182 183 name 184 A unique string to serve as the state's ID 185 186 changes : True 187 Controls whether or not the state reports that there were changes. 188 There are three supported values for this argument: 189 190 - If ``True``, the state will report changes 191 - If ``False``, the state will report no changes 192 - If ``"Random"``, the state will randomly report either changes or no 193 changes. 194 195 result : True 196 Controls the result for for the state. Like ``changes``, there are 197 three supported values for this argument: 198 199 - If ``True``, the state will report a ``True`` result 200 - If ``False``, the state will report a ``False`` result 201 - If ``"Random"``, the state will randomly report either ``True`` 202 203 .. note:: 204 The result will be reported as ``None`` if *all* of the following 205 are true: 206 207 1. The state is being run in test mode (i.e. ``test=True`` on the 208 CLI) 209 210 2. ``result`` is ``True`` (either explicitly, or via being set to 211 ``"Random"``) 212 213 3. ``changes`` is ``True`` (either explicitly, or via being set to 214 ``"Random"``) 215 216 comment : "" 217 Comment field field for the state. By default, this is an empty string. 218 219 warnings 220 A string (or a list of strings) to fill the warnings field with. 221 Default is None 222 223 .. versionadded:: 3000 224 """ 225 ret = {"name": name, "changes": {}, "result": False, "comment": comment} 226 change_data = { 227 "testing": {"old": "Unchanged", "new": "Something pretended to change"} 228 } 229 230 if changes is True: 231 # If changes is True we place our dummy change dictionary into it. 232 # Following the docs as written here 233 # https://docs.saltproject.io/ref/states/writing.html#return-data 234 ret["changes"] = change_data 235 elif changes is False: 236 # Don't modify changes from the "ret" dict set above 237 pass 238 else: 239 if str(changes).lower() == "random": 240 if random.choice((True, False)): 241 # Following the docs as written here 242 # https://docs.saltproject.io/ref/states/writing.html#return-data 243 ret["changes"] = change_data 244 else: 245 err = ( 246 "You have specified the state option 'changes' with " 247 "invalid arguments. It must be either " 248 "True, False, or 'Random'" 249 ) 250 raise SaltInvocationError(err) 251 252 if isinstance(result, bool): 253 ret["result"] = result 254 else: 255 if str(result).lower() == "random": 256 ret["result"] = random.choice((True, False)) 257 else: 258 raise SaltInvocationError( 259 "You have specified the state option " 260 "'result' with invalid arguments. It must " 261 "be either True, False, or " 262 "'Random'" 263 ) 264 265 if warnings is None: 266 pass 267 elif isinstance(warnings, str): 268 ret["warnings"] = [warnings] 269 elif isinstance(warnings, list): 270 ret["warnings"] = warnings 271 else: 272 raise SaltInvocationError( 273 "You have specified the state option " 274 "'warnings' with invalid arguments. It must " 275 "be a string or a list of strings" 276 ) 277 278 if __opts__["test"]: 279 ret["result"] = True if changes is False else None 280 ret["comment"] = "This is a test" if not comment else comment 281 282 return ret 283 284 285def show_notification(name, text=None, **kwargs): 286 """ 287 .. versionadded:: 2015.8.0 288 289 Simple notification using text argument. 290 291 name 292 A unique string to serve as the state's ID 293 294 text 295 Text to return in the comment field 296 """ 297 298 if not text: 299 raise SaltInvocationError("Missing required argument text.") 300 301 ret = {"name": name, "changes": {}, "result": True, "comment": text} 302 303 return ret 304 305 306def mod_watch(name, sfun=None, **kwargs): 307 """ 308 Call this function via a watch statement 309 310 .. versionadded:: 2014.7.0 311 312 Any parameters in the state return dictionary can be customized by adding 313 the keywords ``result``, ``comment``, and ``changes``. 314 315 .. code-block:: yaml 316 317 this_state_will_return_changes: 318 test.succeed_with_changes 319 320 this_state_will_NOT_return_changes: 321 test.succeed_without_changes 322 323 this_state_is_watching_another_state: 324 test.succeed_without_changes: 325 - comment: 'This is a custom comment' 326 - watch: 327 - test: this_state_will_return_changes 328 - test: this_state_will_NOT_return_changes 329 330 this_state_is_also_watching_another_state: 331 test.succeed_without_changes: 332 - watch: 333 - test: this_state_will_NOT_return_changes 334 """ 335 has_changes = [] 336 if "__reqs__" in __low__: 337 for req in __low__["__reqs__"]["watch"]: 338 tag = _gen_tag(req) 339 if __running__[tag]["changes"]: 340 has_changes.append("{state}: {__id__}".format(**req)) 341 342 ret = { 343 "name": name, 344 "result": kwargs.get("result", True), 345 "comment": kwargs.get("comment", "Watch statement fired."), 346 "changes": kwargs.get("changes", {"Requisites with changes": has_changes}), 347 } 348 return ret 349 350 351def _check_key_type(key_str, key_type=None): 352 """ 353 Helper function to get pillar[key_str] and 354 check if its type is key_type 355 356 Returns None if the pillar key is missing. 357 If present True or False depending on match 358 of the values type. 359 360 Can't check for None. 361 """ 362 value = __salt__["pillar.get"](key_str, None) 363 if value is None: 364 return None 365 elif key_type is not None and not isinstance(value, key_type): 366 return False 367 else: 368 return True 369 370 371def _if_str_then_list(listing): 372 """ 373 Checks if its argument is a list or a str. 374 A str will be turned into a list with the 375 str as its only element. 376 """ 377 if isinstance(listing, str): 378 return [salt.utils.stringutils.to_unicode(listing)] 379 elif not isinstance(listing, list): 380 raise TypeError 381 return salt.utils.data.decode_list(listing) 382 383 384def check_pillar( 385 name, 386 present=None, 387 boolean=None, 388 integer=None, 389 string=None, 390 listing=None, 391 dictionary=None, 392 verbose=False, 393): 394 """ 395 Checks the presence and, optionally, the type of given keys in Pillar 396 397 Supported kwargs for types are: 398 - boolean (bool) 399 - integer (int) 400 - string (str) 401 - listing (list) 402 - dictionary (dict) 403 404 Checking for None type pillars is not implemented yet. 405 406 .. code-block:: yaml 407 408 is-pillar-foo-present-and-bar-is-int: 409 test.check_pillar: 410 - present: 411 - foo 412 - integer: 413 - bar 414 """ 415 if not (present or boolean or integer or string or listing or dictionary): 416 raise SaltInvocationError("Missing required argument text.") 417 418 present = present or [] 419 boolean = boolean or [] 420 integer = integer or [] 421 string = string or [] 422 listing = listing or [] 423 dictionary = dictionary or [] 424 425 ret = {"name": name, "changes": {}, "result": True, "comment": ""} 426 checks = {} 427 fine = {} 428 failed = {} 429 # for those we don't check the type: 430 present = _if_str_then_list(present) 431 checks[None] = present 432 # those should be bool: 433 boolean = _if_str_then_list(boolean) 434 checks[bool] = boolean 435 # those should be int: 436 437 # those should be integer: 438 integer = _if_str_then_list(integer) 439 checks[int] = integer 440 # those should be str: 441 string = _if_str_then_list(string) 442 checks[(str,)] = string 443 # those should be list: 444 listing = _if_str_then_list(listing) 445 checks[list] = listing 446 # those should be dict: 447 dictionary = _if_str_then_list(dictionary) 448 checks[dict] = dictionary 449 450 for key_type, keys in checks.items(): 451 for key in keys: 452 result = _check_key_type(key, key_type) 453 if result is None: 454 failed[key] = None 455 ret["result"] = False 456 elif not result: 457 failed[key] = key_type 458 ret["result"] = False 459 elif verbose: 460 fine[key] = key_type 461 462 for key, key_type in failed.items(): 463 comment = 'Pillar key "{}" '.format(key) 464 if key_type is None: 465 comment += "is missing.\n" 466 else: 467 comment += "is not {}.\n".format(key_type) 468 ret["comment"] += comment 469 470 if verbose and fine: 471 comment = "Those keys passed the check:\n" 472 for key, key_type in fine.items(): 473 comment += "- {} ({})\n".format(key, key_type) 474 ret["comment"] += comment 475 476 return ret 477