1""" 2Loop state 3 4Allows for looping over execution modules. 5 6.. versionadded:: 2017.7.0 7 8In both examples below, the execution module function ``boto_elb.get_instance_health`` 9returns a list of dicts. The condition checks the ``state``-key of the first dict 10in the returned list and compares its value to the string `InService`. 11 12.. code-block:: yaml 13 14 wait_for_service_to_be_healthy: 15 loop.until: 16 - name: boto_elb.get_instance_health 17 - condition: m_ret[0]['state'] == 'InService' 18 - period: 5 19 - timeout: 20 20 - m_args: 21 - {{ elb }} 22 - m_kwargs: 23 keyid: {{ access_key }} 24 key: {{ secret_key }} 25 instances: "{{ instance }}" 26 27.. warning:: 28 29 This state allows arbitrary python code to be executed through the condition 30 parameter which is literally evaluated within the state. Please use caution. 31 32.. versionchanged:: 3000 33 34A version that does not use eval is now available. It uses either the python ``operator`` 35to compare the result of the function called in ``name``, which can be one of the 36following: lt, le, eq (default), ne, ge, gt. 37Alternatively, `compare_operator` can be filled with a function from an execution 38module in ``__salt__`` or ``__utils__`` like the example below. 39The function :py:func:`data.subdict_match <salt.utils.data.subdict_match>` checks if the 40``expected`` expression matches the data returned by calling the ``name`` function 41(with passed ``args`` and ``kwargs``). 42 43.. code-block:: yaml 44 45 Wait for service to be healthy: 46 loop.until_no_eval: 47 - name: boto_elb.get_instance_health 48 - expected: '0:state:InService' 49 - compare_operator: data.subdict_match 50 - period: 5 51 - timeout: 20 52 - args: 53 - {{ elb }} 54 - kwargs: 55 keyid: {{ access_key }} 56 key: {{ secret_key }} 57 instances: "{{ instance }}" 58""" 59 60 61import logging 62import operator 63import sys 64import time 65 66# Initialize logging 67log = logging.getLogger(__name__) 68 69# Define the module's virtual name 70__virtualname__ = "loop" 71 72 73def __virtual__(): 74 return True 75 76 77def until(name, m_args=None, m_kwargs=None, condition=None, period=1, timeout=60): 78 """ 79 Loop over an execution module until a condition is met. 80 81 :param str name: The name of the execution module 82 :param list m_args: The execution module's positional arguments 83 :param dict m_kwargs: The execution module's keyword arguments 84 :param str condition: The condition which must be met for the loop to break. 85 This should contain ``m_ret`` which is the return from the execution module. 86 :param period: The number of seconds to wait between executions 87 :type period: int or float 88 :param timeout: The timeout in seconds 89 :type timeout: int or float 90 """ 91 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 92 93 if m_args is None: 94 m_args = () 95 if m_kwargs is None: 96 m_kwargs = {} 97 98 if name not in __salt__: 99 ret["comment"] = "Cannot find module {}".format(name) 100 elif condition is None: 101 ret["comment"] = "An exit condition must be specified" 102 elif not isinstance(period, (int, float)): 103 ret["comment"] = "Period must be specified as a float in seconds" 104 elif not isinstance(timeout, (int, float)): 105 ret["comment"] = "Timeout must be specified as a float in seconds" 106 elif __opts__["test"]: 107 ret["comment"] = "The execution module {} will be run".format(name) 108 ret["result"] = None 109 else: 110 if m_args is None: 111 m_args = [] 112 if m_kwargs is None: 113 m_kwargs = {} 114 115 timeout = time.time() + timeout 116 while time.time() < timeout: 117 m_ret = __salt__[name](*m_args, **m_kwargs) 118 if eval(condition): # pylint: disable=W0123 119 ret["result"] = True 120 ret["comment"] = "Condition {} was met".format(condition) 121 break 122 time.sleep(period) 123 else: 124 ret["comment"] = "Timed out while waiting for condition {}".format( 125 condition 126 ) 127 return ret 128 129 130def until_no_eval( 131 name, 132 expected, 133 compare_operator="eq", 134 timeout=60, 135 period=1, 136 init_wait=0, 137 args=None, 138 kwargs=None, 139): 140 """ 141 Generic waiter state that waits for a specific salt function to produce an 142 expected result. 143 The state fails if the function does not exist or raises an exception, 144 or does not produce the expected result within the allotted retries. 145 146 :param str name: Name of the module.function to call 147 :param expected: Expected return value. This can be almost anything. 148 :param str compare_operator: Operator to use to compare the result of the 149 module.function call with the expected value. This can be anything present 150 in __salt__ or __utils__. Will be called with 2 args: result, expected. 151 :param timeout: Abort after this amount of seconds (excluding init_wait). 152 :type timeout: int or float 153 :param period: Time (in seconds) to wait between attempts. 154 :type period: int or float 155 :param init_wait: Time (in seconds) to wait before trying anything. 156 :type init_wait: int or float 157 :param list args: args to pass to the salt module.function. 158 :param dict kwargs: kwargs to pass to the salt module.function. 159 160 .. versionadded:: 3000 161 162 """ 163 ret = {"name": name, "comment": "", "changes": {}, "result": False} 164 if name not in __salt__: 165 ret["comment"] = 'Module.function "{}" is unavailable.'.format(name) 166 elif not isinstance(period, (int, float)): 167 ret["comment"] = "Period must be specified as a float in seconds" 168 elif not isinstance(timeout, (int, float)): 169 ret["comment"] = "Timeout must be specified as a float in seconds" 170 elif compare_operator in __salt__: 171 comparator = __salt__[compare_operator] 172 elif compare_operator in __utils__: 173 comparator = __utils__[compare_operator] 174 elif not hasattr(operator, compare_operator): 175 ret["comment"] = 'Invalid operator "{}" supplied.'.format(compare_operator) 176 else: 177 comparator = getattr(operator, compare_operator) 178 if __opts__["test"]: 179 ret["result"] = None 180 ret["comment"] = 'Would have waited for "{}" to produce "{}".'.format( 181 name, expected 182 ) 183 if ret["comment"]: 184 return ret 185 186 if init_wait: 187 time.sleep(init_wait) 188 if args is None: 189 args = [] 190 if kwargs is None: 191 kwargs = {} 192 193 res_archive = [] 194 current_attempt = 0 195 timeout = time.time() + timeout 196 while time.time() < timeout: 197 current_attempt += 1 198 try: 199 res = __salt__[name](*args, **kwargs) 200 except Exception: # pylint: disable=broad-except 201 (exc_type, exc_value, _) = sys.exc_info() 202 ret["comment"] = "Exception occurred while executing {}: {}:{}".format( 203 name, exc_type, exc_value 204 ) 205 break 206 res_archive.append(res) 207 cmp_res = comparator(res, expected) 208 log.debug( 209 "%s:until_no_eval:\n" 210 "\t\tAttempt %s, result: %s, expected: %s, compare result: %s", 211 __name__, 212 current_attempt, 213 res, 214 expected, 215 cmp_res, 216 ) 217 if cmp_res: 218 ret["result"] = True 219 ret["comment"] = "Call provided the expected results in {} attempts".format( 220 current_attempt 221 ) 222 break 223 time.sleep(period) 224 else: 225 ret[ 226 "comment" 227 ] = "Call did not produce the expected result after {} attempts".format( 228 current_attempt 229 ) 230 log.debug( 231 "%s:until_no_eval:\n\t\tResults of all attempts: %s", 232 __name__, 233 res_archive, 234 ) 235 return ret 236